<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기술 블로그</title>
    <link>https://youbin2.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 25 Jun 2026 22:39:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hongyb</managingEditor>
    <item>
      <title>리눅스 진단 명령어</title>
      <link>https://youbin2.tistory.com/43</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스 진단 명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 OS 상황을 모니터 해야 할 경우도 있다. 시스템 장애가 애플리케이션에만 있는 것이 아니기 때문이다. 리눅스 진단에 사용할 수 있는 명령어를 알아보고 OS 상황을 모니터링할 때 활용해 보자.&lt;/p&gt;
&lt;p 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;2400&quot; data-origin-height=&quot;1800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pwj8v/btsOEBwrw8P/W4Rnv9cTIkktYt4GthnBgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pwj8v/btsOEBwrw8P/W4Rnv9cTIkktYt4GthnBgk/img.png&quot; data-alt=&quot;https://www.brendangregg.com/linuxperf.html &amp;amp;nbsp;[Brendan Gregg 블로그]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pwj8v/btsOEBwrw8P/W4Rnv9cTIkktYt4GthnBgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpwj8v%2FbtsOEBwrw8P%2FW4Rnv9cTIkktYt4GthnBgk%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;2400&quot; height=&quot;1800&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.brendangregg.com/linuxperf.html &amp;nbsp;[Brendan Gregg 블로그]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에는 Brendan Gregg의 블로그에서 가져온 리눅스 분석 도구 표이다. 중요한 분석 도구부터 차례대로 알아보도록 하겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU 모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU를 모니터링할 때 mpstat을 사용한다. mpstat이란 사용 가능한 CPU 사용 상황을 제공한다. 데이터 출력 간격과 수행 횟수를 지정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1750087897254&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mpstat 5 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 [데이터 출력 간격] [데이터 수행 횟수] 순으로 지정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력된 결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZQhId/btsOEAYAcW8/Q23MrKFseQVgE8y1MXdxg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZQhId/btsOEAYAcW8/Q23MrKFseQVgE8y1MXdxg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZQhId/btsOEAYAcW8/Q23MrKFseQVgE8y1MXdxg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZQhId%2FbtsOEAYAcW8%2FQ23MrKFseQVgE8y1MXdxg1%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;1816&quot; height=&quot;158&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진 속 결과 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;%usr : 애플리케이션에서 수행되는 동안 사용한 CPU 시간 비율&lt;/li&gt;
&lt;li&gt;%nice : nice 우선순위로 애플리케이션 레벨에서 수행되는 동안 사용한 CPU 시간 비율&lt;/li&gt;
&lt;li&gt;%sys : 커널 레벨에서 수행되는 동안 사용한 CPU 시간 비율. 인터럽트 비용은 포함되지 않는다.&lt;/li&gt;
&lt;li&gt;%iowait : 시스템 디스크의 I/O 요청을 처리하는 동안 CPU가 유휴 상태인 시간 비율&lt;/li&gt;
&lt;li&gt;%irq : 하드웨어 인터럽트를 CPU에서 처리하는 데 사용한 CPU 시간 비율&lt;/li&gt;
&lt;li&gt;%soft : 소프트웨어 인터럽트를 CPU에서 처리하는 데 사용한 CPU 시간 비율&lt;/li&gt;
&lt;li&gt;%steal : 하이퍼바이저가 가상 프로세서 처리할 때 CPU에서 대기하는 데 사용한 시간 비율&lt;/li&gt;
&lt;li&gt;%guest : 가산 프로세서 수행하기 위해 사용한 시간 비율&lt;/li&gt;
&lt;li&gt;%idle : 유휴 상태인 시간 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;nice란 사용자가 nice라는 명령어를 통해 프로세스 우선순위 변경할 수 있는데 그 프로세스에 의해 수행된 시간을 의미한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;mpstat 옵션&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'I' 옵션은 인터럽트 통계 제공한다. 여기서 SUM, CPU, ALL 중 하나를 지정해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088075074&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mpstat -I [SUM | CPU | ALL]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SUM은 모든 CPU의 초당 인터럽트 수의 합&lt;/li&gt;
&lt;li&gt;CPU는 각 CPU 별로 상세한 인터럽트 정보 제공&lt;/li&gt;
&lt;li&gt;ALL은 둘을 합친 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750088104906&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mpstat -I SUM&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0dBHA/btsOCAy0q8K/roEQ4DKjqkqnReD75FiMs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0dBHA/btsOCAy0q8K/roEQ4DKjqkqnReD75FiMs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0dBHA/btsOCAy0q8K/roEQ4DKjqkqnReD75FiMs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0dBHA%2FbtsOCAy0q8K%2FroEQ4DKjqkqnReD75FiMs1%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;1812&quot; height=&quot;158&quot; data-origin-width=&quot;1812&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;intr/s는 초당 CPU가 받은 인터럽트 수이다. 이외에 다른 옵션은 mpstat 매뉴얼을 참고하자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CPU 모니터링 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 모니터링은 다음 순서대로 진행하자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;User CPU&lt;/li&gt;
&lt;li&gt;System CPU&lt;/li&gt;
&lt;li&gt;I/O Wait CPU&lt;/li&gt;
&lt;li&gt;기타 CPU&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 애플리케이션에서 (User CPU : System CPU) 사용 비율은 10:1 ~ 8:1 정도라고 볼 수 있다. 만약 이 비율이 1:1이나 3:2 정도라면 해당 애플리케이션이 커널 CPU를 많이 쓴다는 의미가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O Wait가 높다면 자바 애플리케이션에서 I/O 작업이 많다는 의미이다. 어디서 I/O가 많은지 찾아야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전반적인 상황 모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vmstat : 가상 메모리 통계를 제공한다. 프로세스, 메모리, 페이장, 블록 I/O, 트랩스, 디스크, CPU 정보를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령어를 실행하면 5초 간격으로 데이터를 출력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088160861&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vmstat 5&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kF9Ps/btsODFT8Zlg/53jAkd97yAwwGQmymiPuoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kF9Ps/btsODFT8Zlg/53jAkd97yAwwGQmymiPuoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kF9Ps/btsODFT8Zlg/53jAkd97yAwwGQmymiPuoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkF9Ps%2FbtsODFT8Zlg%2F53jAkd97yAwwGQmymiPuoK%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;1426&quot; height=&quot;208&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 5초간의 평균값이고, 첫 번째는 시스템 부팅된 이후의 통곗값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;procs
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;r : 실행하기 위해 대기 중인 프로세스 수&lt;/li&gt;
&lt;li&gt;b : uninterruptible sleep 상태에 있는 프로세스 수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;memory
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swpd : 가상 메모리 사용한 크기&lt;/li&gt;
&lt;li&gt;free : 사용하지 않는 메모리 크기&lt;/li&gt;
&lt;li&gt;buff : 버퍼로 사용하는 메모리 크기&lt;/li&gt;
&lt;li&gt;cache : 캐시로 사용하는 메모리 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;swap
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;si : 초당 디스크에서 읽은 스왑 된 메모리 크기&lt;/li&gt;
&lt;li&gt;so : 초당 디스크로 스왑 된 메모리 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;io
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bi : 블록 디바이스에서 받은 블록 수&lt;/li&gt;
&lt;li&gt;bo : 블록 디바이스로 보낸 블록 수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;system
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in : clock 포함한 초당 인터럽트 수&lt;/li&gt;
&lt;li&gt;cs : 초당 콘텍스트 전환 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;cpu
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;us : 사용자 코드 수행하는데 소요된 시간&lt;/li&gt;
&lt;li&gt;sy : 커널 코드 수행하는데 소요된 시간&lt;/li&gt;
&lt;li&gt;id : idle 상태에서 수행된 시간&lt;/li&gt;
&lt;li&gt;wa : io 대기하는데 소요된 시간&lt;/li&gt;
&lt;li&gt;st : 가상 머신에 뺏긴 시간&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로세스별 CPU 사용량 모니터링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스별 CPU 사용량 모니터링 명령어는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088259510&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pidstat&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QI2MM/btsOEDufGTi/kbLVtHAweWu9BAWij6Sdu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QI2MM/btsOEDufGTi/kbLVtHAweWu9BAWij6Sdu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QI2MM/btsOEDufGTi/kbLVtHAweWu9BAWij6Sdu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQI2MM%2FbtsOEDufGTi%2FkbLVtHAweWu9BAWij6Sdu1%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;1436&quot; height=&quot;192&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디스크 사용량&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 사용량 명령어는 df와 du명령어가 있다. 우선 df 명령어부터 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1750088326090&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGIxkn/btsODYMmZhL/9pB7g24WvQ8MtIop1On02k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGIxkn/btsODYMmZhL/9pB7g24WvQ8MtIop1On02k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGIxkn/btsODYMmZhL/9pB7g24WvQ8MtIop1On02k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGIxkn%2FbtsODYMmZhL%2F9pB7g24WvQ8MtIop1On02k%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;1434&quot; height=&quot;196&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Use%는 사용률을 뜻하는데 사용률이 100%가 되면 다음 에러가 발생할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그가 쓰이지 않아 hang이 발생&lt;/li&gt;
&lt;li&gt;컴파일 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;du 명령어는 파일과 디렉터리 점유 상황을 보여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088379324&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;du -s [디렉터리 경로]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IO 사용량&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;iostat&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IO 모니터링 도구는 iostat이다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088544891&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;iostat&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duQBbo/btsOD2Vy6XC/PpS5E46Kl2D6T38vXg4OH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duQBbo/btsOD2Vy6XC/PpS5E46Kl2D6T38vXg4OH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duQBbo/btsOD2Vy6XC/PpS5E46Kl2D6T38vXg4OH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduQBbo%2FbtsOD2Vy6XC%2FPpS5E46Kl2D6T38vXg4OH1%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;1456&quot; height=&quot;306&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 부팅 이후 지금까지 통계 정보를 보여준다. 수행 시간 간격을 지정하고 싶다면 옵션으로 간격 수행 횟수를 붙여주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음을 뜻한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tps : 디바이스에 초당 전송 요청한 건수&lt;/li&gt;
&lt;li&gt;read/s : 디바이스에 초당 읽은 데이터 블록 단위&lt;/li&gt;
&lt;li&gt;wrtn/s : 디바이스에서 초당 쓴 데이터 블록 단위&lt;/li&gt;
&lt;li&gt;read : 디바이스에서 지정한 간격 동안 읽은 블록 수&lt;/li&gt;
&lt;li&gt;wrtn : 디바이스에서 지정한 간격 동안 쓴 전체 블록 수&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;lsof&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lsof는 특정 프로세스에서 어떤 IO가 발생하는지 알려주는 명령어다. lsof에서 쓸 만한 옵션이 2가지 정도 있다. 첫 번째는 -p 옵션이다. 해당 pid를 갖는 프로세스에서 참조하는 파일의 목록을 보여준다. -c 옵션은 &amp;lsquo;명령어 이름&amp;rsquo;과 같은 명령어로 수행된 프로세스에서 참조하는 파일 목록을 보여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1750088608123&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lsof -p [pid]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJpR2F/btsOEZjyeny/3zFzskXYKhpviKMi0bM6O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJpR2F/btsOEZjyeny/3zFzskXYKhpviKMi0bM6O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJpR2F/btsOEZjyeny/3zFzskXYKhpviKMi0bM6O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeJpR2F%2FbtsOEZjyeny%2F3zFzskXYKhpviKMi0bM6O0%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;1442&quot; height=&quot;176&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목의 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;command : 명령어 이름&lt;/li&gt;
&lt;li&gt;PID : 프로세스 아이디&lt;/li&gt;
&lt;li&gt;USER : 명령어 수행한 사용자 아이디&lt;/li&gt;
&lt;li&gt;FD : 파일 설명이나 상태&lt;/li&gt;
&lt;li&gt;TYPE : 파일 타입&lt;/li&gt;
&lt;li&gt;DEVICE : 디바이스 정보&lt;/li&gt;
&lt;li&gt;SIZE/OFF : 파일 크기&lt;/li&gt;
&lt;li&gt;NODE : 로컬 파일 노드 정보&lt;/li&gt;
&lt;li&gt;NAME : 파일 이름&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>리눅스</category>
      <category>명령어</category>
      <category>진단 명령어</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/43</guid>
      <comments>https://youbin2.tistory.com/43#entry43comment</comments>
      <pubDate>Tue, 17 Jun 2025 00:47:15 +0900</pubDate>
    </item>
    <item>
      <title>Java 메모리 진단</title>
      <link>https://youbin2.tistory.com/42</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;Java는 Garbage Collector(GC)가 알아서 사용하지 않는 객체를 메모리에서 제거한다. 그래서 메모리 릭 현상이 간간히 발생하고, 원인을 잡기 쉽지 않다. 메모리 릭이 발생하면 원인을 어떻게 찾아야 하는지 알아보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java 메모리 영역&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바의 메모리 영역은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;PC 레지스터 : 스레드의 JVM인스트럭션 주소가 저장되어 있다. 스레드별 각각 보유한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;JVM 스택 : 지역 변수와 부분 결과를 저장한다. 메서드 호출 및 리턴 정보 보관된다. 스레드별 각각 보유한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;힙 : 인스턴스와 배열이 할당된다. GC에 의해 관리된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;메서드 영역 : 모든 JVM 스레드 공유하며, 클래스 구조 정보 저장한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;런타임 상수 풀 : 클래스 및 인터페이스에 대한 constant_pool 테이블을 실행 시 참고하기 위한 저장소이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;네이티브 메서드 스택 : 자바 언어 이외의 네이티브 언어를 호출할 경우 타 언어 스택 정보 저장한다.&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OutOfMemoryError란&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OutOfMemoryError는 가비지 컬렉터가 새로운 객체를 생성할 공간을 만들지 못하고, 힙 영역 메모리가 증가될 수 없을 때 발생한다. OutOfMemoryError가 왜 발생하는지 원인을 알고 싶으면 에러 로그를 잘 확인해봐야 한다. 에러로그를 살펴보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Exception in thread &amp;ldquo;majava.lang.OutOfMemoryError: Java heap space&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 크기를 너무 적게 잡아 놓거나, 메모리 크기를 지정하지 않은 경우 : 자바 옵션 중 -Xms는 JVM메모리 최소 크기를, -Xmx는 최대 메모리 크기를 지정한다.&lt;/li&gt;
&lt;li&gt;오래된 객체들이 계속 참조되고 있어서 GC가 되지 않는 경우 : static 잘못 사용하거나 애플리케이션이 객체를 지속해서 참조할 경우 &amp;rarr; 메모리 릭 발생&lt;/li&gt;
&lt;li&gt;스레드 우선순위 높일 경우 : GC 처리속도보다 우선순위 높은 스레드가 메모리에 생성하는 속도가 빨라 문제 발생&lt;/li&gt;
&lt;li&gt;큰 덩어리 객체 여러 개 있을 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Exception in thread &amp;ldquo;main&amp;rdquo;:java.lang.OutOfMemoryError: Metaspace&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 클래스가 자바 프로세스에 로딩될 경우에 발생한다. 메타 영역 크기보다 크게 지정하면 된다. 옵션은 -XX:MaxMetaspaceSize=128m를 사용하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Exception in thread &amp;ldquo;main&amp;rdquo;:java.lang.OutOfMemoryError: Requested array size exceeds VM limit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 크기가 힙 영역 코기보다 크게 지정되었을 때 발생&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4. Exception in thread &amp;ldquo;main&amp;rdquo;:java.lang.OutOfMemoryError: request &amp;lt;size&amp;gt; bytes for &amp;lt;reason&amp;gt;. Out of swap space&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;네이티브 힙 영역이 부족할 때 발생한다. 즉 OS의 메모리가 부족한 경우이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Exception in thread &amp;ldquo;main&amp;rdquo;:java.lang.OutOfMemoryError: &amp;lt;reason&amp;gt; &amp;lt;stacktrace&amp;gt; (Native method)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 힙 영역 메모리 할당할 때 발생하는 메시지이다. 앞은 JVM코드에서 발견될 때 발생하고 이 경우 JNI나 네이티브 코드에서 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[메모리 릭]&lt;br /&gt;메모리 릭이란 객체가 GC 되지 않아 메모리가 부족해지는 현상이다. 코드나 클래스 로더 문제일 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잦은 GC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OutOfMemoryError 이외에도 잦은 GC에 의해 문제 발생할 수 있다. GC는 Minot GC와 Full GC 두 가지가 있으며 보통 Full GC가 많이 발생하면 성능에 영향을 미친다. 다음 규칙을 따르면 GC를 발생시키지 않을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 메모리 사용 최소화&lt;/li&gt;
&lt;li&gt;객체 재사용&lt;/li&gt;
&lt;li&gt;XML 처리 시 메모리 점유하는 DOM보다 SAX 사용&lt;/li&gt;
&lt;li&gt;많은 데이터 한 번에 보여주는 비즈니스 로직 제거&lt;/li&gt;
&lt;li&gt;임시 메모리 많이 사용하는 부분 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 GC가 자주 발생하는 원인은 여러 가지가 있다. 운영하는 서비스에 동시에 1000명이 넘는 사용자가 접속하는 경우 Full GC가 자주 발생하는 것은 당연한 현상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;jstat 명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 단면(힙 덤프)은 메모리가 부족해지는 현상이 지속될 때와 OutOfMemoryError가 발생했을 때 생성해야 한다. 그러면 메모리가 부족해지는지 어떻게 알 수 있을까? jstat 명령어로 간단하게 확인할 수 있다.&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;pre id=&quot;code_1749567822055&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jstat -gcutil [pid] 1s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVwuVD/btsOvwJCpnT/fVP8ii5ybqEKIYIU0erZyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVwuVD/btsOvwJCpnT/fVP8ii5ybqEKIYIU0erZyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVwuVD/btsOvwJCpnT/fVP8ii5ybqEKIYIU0erZyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVwuVD%2FbtsOvwJCpnT%2FfVP8ii5ybqEKIYIU0erZyk%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;1494&quot; height=&quot;346&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E는 Eden영역, S0과 S1은 Survivor영역이다. 두 영역은 Young으로 묶을 수 있다. O는 Old영역으로 Tenured 영역으로 나뉜다. 이 Tenured영역이 GC이후에도 증가하는지 확인하면 된다.&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;보통 Tenured영역은 애플리케이션 동작중에 계속 증가하는 것이 기본이다. 하지만 Full GC이후에도 메모리 사용량이 증가하고 있다면 메모리 릭이 발생하고 있다는 증거이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에 메모리 단면은 확인해야 하지만 한 가지 주의할 점이 있다. 메모리 사용량 분석할 때 성능에 많은 영향을 주기 때문에 운영 서버에서 확인하면 안 된다. 개발자의 PC나 개발 서버에서 문제점을 확인해야 한다. 메모리 덤프 시 다음 현상이 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;덤프 파일 생성하는 동안 서비스 불가능&lt;/li&gt;
&lt;li&gt;덤프 생성 시 많은 시간 소요&lt;/li&gt;
&lt;li&gt;큰 파일이 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 단면은 JVM에 있는 객체 하나하나에 대한 정보를 담고 있다. 그래서 생성하는데 오래 걸리고 시스템 운영에 문제가 발생한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 단면 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jdk에서 지원하는 jmap 명령어로 힙 덤프를 생성할 수 있다. jmap 필요 옵션은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;-dump:[live,] format=b, file=&amp;lt;filename&amp;gt; : 메모리 단면 생성. live옵션은 살아있는 객체만 덤프 하라는 뜻. format을 b로 지정 시 바이너리 형태 파일 생성. file옵션에 지정된 파일 이름으로 생성. 예시) jmap -dump:format=b, file=goldmem.bin [pid]&lt;/li&gt;
&lt;li&gt;-finalizerinfo : GC가 되려고 기다리고 있는 객체 정보 출력&lt;/li&gt;
&lt;li&gt;-clstats : 클래스 로더의 통계 정보를 제공&lt;/li&gt;
&lt;li&gt;-histo[:live] : 힙 영역의 메모리 점유 상태를 가장 많이 점유한 객체부터 출력&lt;/li&gt;
&lt;li&gt;-F : -dump와 -histo 옵션과 같이 사용. 덤프가 발생되지 않을 경우 강제 발생시킬 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;histo 옵션으로 실행하면 결과는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1749567959275&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jmap -histo [pid]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGFtGV/btsOvwbL0tr/0Vib0Y97iqldkdidtonuA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGFtGV/btsOvwbL0tr/0Vib0Y97iqldkdidtonuA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGFtGV/btsOvwbL0tr/0Vib0Y97iqldkdidtonuA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGFtGV%2FbtsOvwbL0tr%2F0Vib0Y97iqldkdidtonuA1%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;1730&quot; height=&quot;194&quot; data-origin-width=&quot;1730&quot; data-origin-height=&quot;194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리를 가장 많이 점유한 객체부터 데이터를 출력한다. 순번, 인스턴스 수 바이트 크기, 클리스 이름 순서로 나열되고 있다. 대문자로 시작하는 클래스는 배열 형태의 클래스를 의미하고 &amp;lt;&amp;gt;로 둘러싸인 클래스들은 JVM에서 사용하는 것이다.&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;몇몇의 JDK에서 jmap을 두 번 이상 사용할 수 없다. 리눅스에서 그런 경우가 발생하면, gcore로 코어 덤프를 발생시키고 그 덤프에 jmap을 사용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749568003107&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gcore -o holdmem.core [pid]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 명령어를 입력하면 holdmem.core.[pid] 라는 파일이 생성된다. 파일이름뒤에 .pid 형식으로 프로세스 id가 붙는다. 이 파일을 jmap을 활용하여 메모리 덤프를 생성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1749568030882&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jmap [옵션] [자바 실팽 파일 위치] [코어덤프 파일]
ex) jmap -dump:format=b, file=holdmem.dump /usr/local/java/bin/java holdmem.core.1975&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자동으로 힙 덤프 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 옵션을 주면 OutOfMemoryError발생 시 힙 덤프가 생성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749568100815&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError&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;pre id=&quot;code_1749568117410&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-XX:HeapDumpPath=[경로]&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;pre id=&quot;code_1749568135082&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp&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;다음 옵션은 OutOfMemoryError발생 시 실행되는 스크립트를 명시할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1749568150048&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-XX:OnOutOfMemoryError=&quot;명령어&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;다음과 같이 활용하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1749568164557&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -XX:+OnOutOfMemoryError=&quot;gcore -o gcore.dump %p&quot; 클래스 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%p는 현재 실행 중인 자바 프로세스 아이디를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 문제 Case&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 시스템이 느릴 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 느리다고 항상 메모리 단면을 사용하는 것은 아니다. 그래도 원인을 찾지 못할 경우 메모리 분석 단계로 넘어가야 한다. 메모리 때문에 시스템이 느릴 경우에 다음을 의심해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리를 작게 잡아 GC가 자주 발생하는 경우&lt;/li&gt;
&lt;li&gt;임시 메모리 사용하여 GC가 자주 발생하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 jstat을 사용하자. 메모리 크기와 관련됐으면 -gccapacity 옵션을, 사용량과 관련됐으면 -gcutil 옵션을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 애플리케이션 응답하지 않을 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 릭이 발생하면 GC 작업만 실행하고, 애플리케이션이 응답하지 않을 수 있다. 메모리 릭일 때 다음 절차를 따르자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메모리 사용량 확인한다. 메모리 사용량 95% 이상인 경우 메모리 릭 문제 원인일 수 있다.&lt;/li&gt;
&lt;li&gt;메모리 사용량 지속해서 높을 경우 메모리 단면 생성&lt;/li&gt;
&lt;li&gt;apm 사용해 어떤 객체가 죽지 않고 점유되는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Java</category>
      <category>java 메모리</category>
      <category>jmap</category>
      <category>jstat</category>
      <category>메모리 덤프</category>
      <category>힙덤프</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/42</guid>
      <comments>https://youbin2.tistory.com/42#entry42comment</comments>
      <pubDate>Wed, 11 Jun 2025 00:12:07 +0900</pubDate>
    </item>
    <item>
      <title>[자바 트러블 슈팅] 스레드 진단</title>
      <link>https://youbin2.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Java 프로세스에서 스레드 단면을 분석하고 문제를 해결하는 방법을 알아보자.&lt;/p&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;스레드에서 발생할 수 있는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 처리할 때 발생할 수 있는 문제는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;1. 레이스 컨디션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티 스레드 환경에서 &amp;lsquo;공유 데이터&amp;rsquo;에 아무런 록 처리를 하지 않을 경우 문제 발생&lt;/li&gt;
&lt;li&gt;여러 스레드에서 동시에 수정 시 공유 데이터 꼬여서 무한 대기 or 무한 루프&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;2. 데드록&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 개 이상의 스레드에서 서로의 록이 풀리기 기다리는 상황&lt;/li&gt;
&lt;li&gt;4가지 조건이 필요[비선점, 상호배제, 점유와 대기, 순환 참조]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) 스레드 A에서 Data1에 록을 건 상태에서 Data2에 접근하려 한다. 스레드 B에서 Data2에 록을 건 상태에서 Data1에 접근하려 한다.&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;3. 스타베이션&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드가 CPU로부터 일할 기회를 받지 못하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;4. 라이브 록&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답을 요청한 스레드에서 계속해서 응답을 요청하는 작업이 계속되는 경우&lt;/li&gt;
&lt;li&gt;데드록과 달리 CPU를 계속해서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;록 경합을 피하는 방법&lt;/h2&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;1. 코드가 아닌 데이터를 보호&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바에서 메서드를 synchronized로 감싸지 말고 데이터만 감싸라&lt;/li&gt;
&lt;li&gt;데이터만 동시성을 막아야지 코드를 막을 필요는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt; 2. 록 사용에 비싼 계산 하지 마라&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공유 데이터를 사용 시 간단한 로직만 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;3. 록 분리해라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공유 데이터를 하나의 록으로 설정하지 말고 최소한으로 분리해라&lt;/li&gt;
&lt;li&gt;배열 전체가 록으로 걸지 말고 배열 요소들이 다른 록을 갖도록 분산해라&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;4. atomic 작업 사용하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 프로그래밍에서 제공하는 atomic연산을 사용해라&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;5. 동기화된 데이터 구조를 사용하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;atomic연산을 사용할 수 없으면, 내부적으로 atomic을 사용하는 데이터 구조를 사용해라&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;6. 읽기-쓰기 록 디자인 패턴을 사용하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기와 쓰기를 위한 사용자를 분리해서 성능 저하 없이 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;7. 읽기 전용 데이터를 사용하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;록을 걸지 않고 모든 스레드에서 접근할 수 있는 읽기 전용 데이터를 만들어라&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;8. 지역 변수, 스레드 로컬 저장소를 사용하라&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스레드의 로컬에 있는 공유 변수는 다른 것으로 치환 가능하다.&lt;/li&gt;
&lt;li&gt;ex) 배열에 있는 큰 수를 찾을 때 스레드에서 값 증가할 때마다 큰 수를 따로 저장하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;스레드 단면 자르기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 본 스레드의 문제를 찾기 위해 스레드 단면을 분석해야 한다. 시스템에 문제가 발생했을 경우 10~30초 간격으로 최소 열 번 정도 스레드 단면을 생성하자. 다음은 스레드 단면 분석을 해야 하는 경우이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 응답 없을 때&lt;/li&gt;
&lt;li&gt;시스템의 CPU가 떨어지지 않을 때&lt;/li&gt;
&lt;li&gt;애플리케이션 응답 없을 때&lt;/li&gt;
&lt;li&gt;시스템이 내 마음대로 작동하지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;스레드 단면 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 단면은 다음 사진과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WsDfm/btsOb92PVR6/OiV7KH2yBbk7ByNTPrcdWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WsDfm/btsOb92PVR6/OiV7KH2yBbk7ByNTPrcdWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WsDfm/btsOb92PVR6/OiV7KH2yBbk7ByNTPrcdWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWsDfm%2FbtsOb92PVR6%2FOiV7KH2yBbk7ByNTPrcdWk%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;1039&quot; height=&quot;837&quot; data-origin-width=&quot;1039&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 정보는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드 이름&lt;/li&gt;
&lt;li&gt;식별자 : 데몬 스레드일 때만 생성&lt;/li&gt;
&lt;li&gt;스레드 우선순위 : prio 부분에 우선순위를 숫자로 나타낸다. 숫자 높을수록 우선순위 높다.&lt;/li&gt;
&lt;li&gt;스레드 id(tid) : 다른 스레드와 구분되는 스레드 id, 해당 스레드가 점유하는 메모리 주소로 봐도 된다.&lt;/li&gt;
&lt;li&gt;네이티브 스레드 ID(nid) : OS에서 관리하는 스레드의 ID&lt;/li&gt;
&lt;li&gt;스레드 상태
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NEW : 아직 시작되지 않은 상태&lt;/li&gt;
&lt;li&gt;RUNNABLE : 수행 중인 상태&lt;/li&gt;
&lt;li&gt;BLOCKED : 스레드가 잠겨 있어 풀리기 기다리는 상태&lt;/li&gt;
&lt;li&gt;WAITING : 다른 스레드가 특정 작업 수행하여 깨울 때까지 무한정 기다리는 상태&lt;/li&gt;
&lt;li&gt;TIMED_WAITING : 다른 스레드가 특정 작업 수행하여 깨울 때까지 지정된 시간만큼 기다리는 상태&lt;/li&gt;
&lt;li&gt;TERMINATED : 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주소 범위&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 at으로 된 부분은 해당 스레드의 스택 정보이다.&lt;/p&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;스레드 단면 생성 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 명령어를 입력해 프로세스 id를 구한다&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;JPS
&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;이후 다음과 같이 kill 명령어를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;kill -3 [pid]
&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;주의할 점은 스레드 단면 생성을 위해 -3 옵션을 써야 한다. -9 옵션을 사용하면 프로세스가 죽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 명령어를 사용해도 된다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;jstack [pid]
&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;프로세스의 CPU 사용량을 알기 위해 다음 명령어를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;ps -Lf -p [PID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 결과가 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k63dQ/btsOcbfkXLp/WBrXgcwKnMSKOkCk1P2sk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k63dQ/btsOcbfkXLp/WBrXgcwKnMSKOkCk1P2sk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k63dQ/btsOcbfkXLp/WBrXgcwKnMSKOkCk1P2sk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk63dQ%2FbtsOcbfkXLp%2FWBrXgcwKnMSKOkCk1P2sk0%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;1154&quot; height=&quot;152&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UID : 사용자 id&lt;/li&gt;
&lt;li&gt;PID : 프로세스 id&lt;/li&gt;
&lt;li&gt;PPID : 부모 프로세스 id&lt;/li&gt;
&lt;li&gt;LWP : 스레드 id&lt;/li&gt;
&lt;li&gt;C : CPU 사용량&lt;/li&gt;
&lt;li&gt;NLWP : 해당 프로세스에서 사용하는 스레드 수. NLWP 수&lt;/li&gt;
&lt;li&gt;STIME : 프로세스 통제하는 터미널&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;스레드로 문제 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드로 해결할 수 있는 문제는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템이 느린 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 시스템이 항상 느린 경우&lt;/li&gt;
&lt;li&gt;시스템 특정 기능이 느린 경우&lt;/li&gt;
&lt;li&gt;특정 시간에 전체 애플리케이션이 느린 경우&lt;/li&gt;
&lt;li&gt;특정 시간에 특정 애플리케이션이 느린 경우&lt;/li&gt;
&lt;li&gt;특정 기능이 점점 느려질 경우&lt;/li&gt;
&lt;li&gt;특정 사용자만 느려질 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시스템 응답이 없는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 애플리케이션이 응답 없는 경우&lt;/li&gt;
&lt;li&gt;특정 기능이 응답하지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 스레드 단면으로 해결하기 애매한 경우이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외 계속 발생
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 사용자가 특정 기능 수행하면 예외 발생&lt;/li&gt;
&lt;li&gt;특정 사용자가 특정 기능에서만 예외&lt;/li&gt;
&lt;li&gt;특정 시간대에서 전체 애플리케이션 예외&lt;/li&gt;
&lt;li&gt;특정 시간대에 특정 애플리케이션에 예외&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 죽는 경우 스레드 단면만으로 해결하지 못한다. 왜냐하면 그 경우 스레드 단면이 생성되지 않기 대문이다. 시스템이 죽는 문제가 발생했을 때 OnError 옵션을 추가해 실마리를 남기자.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;-XX:OnError=&quot;명령어&quot;
ex) -XX:OnError=&quot;kill -e %p&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;시스템이 느릴 경우 전차&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 느릴 때 다음 순서로 점검하자&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CPU, 메모리 리소스 사용량&lt;/li&gt;
&lt;li&gt;외부와 연동하는 리소스 사용량&lt;/li&gt;
&lt;li&gt;WAS 메모리 및 스레드 설정 및 사용량&lt;/li&gt;
&lt;li&gt;Web 서버 설정 점검&lt;/li&gt;
&lt;li&gt;OS 설정 점검&lt;/li&gt;
&lt;li&gt;스레드 상태 점검&lt;/li&gt;
&lt;li&gt;메모리 상태 점검&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt; 시스템 응답 없을 때 스레드 단면&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 단면은 시스템 응답이 없는 경우에 큰 효과를 보인다. 어떤 스레드에서 응답을 주지 않아 시스템이 멈췄는지 알 수 있다. ex) 스레드 풀이나 DB 커넥션 풀이 꽉 찼을 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 순서로 봐야 할지 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;전체 스레드 개수 확인&lt;/li&gt;
&lt;li&gt;스레드 메모리 사용량 확인&lt;/li&gt;
&lt;li&gt;Lock 확인&lt;/li&gt;
&lt;li&gt;Runnable 스레드 살펴보기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 메모리가 부족해 시스템이 응답하지 않을 수 있다. 이 경우 GC 관련 스레드가 CPU 코어를 하나 이상 100% 점유하고 있다. 이런 상황에 다음 과정을 실행한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스레드 단면 주기적으로 떠놓는다.&lt;/li&gt;
&lt;li&gt;ps -Lf -p pid 명령어도 같이 수행한다.&lt;/li&gt;
&lt;li&gt;2번에서 CPU 사용 시간이 지속해서 증가하는 스레드가 있다면 그 스레드 id 확인 후 스레드 단면에서 해당 스레드를 확인한다.&lt;/li&gt;
&lt;li&gt;3번에서 확인한 스레드가 GC관련 스레드라면, jstat 명령어로 메모리 사용량 확인한다. ex) jstat -gcutil 5s&lt;/li&gt;
&lt;li&gt;GC 관련 스레드가 아니라면 무한 루프에 빠지지 않았는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Java</category>
      <category>DEAD LOCK</category>
      <category>Lock</category>
      <category>데드락</category>
      <category>라이브 락</category>
      <category>락</category>
      <category>스레드</category>
      <category>스레드 단면</category>
      <category>스레드 덤프</category>
      <category>자바</category>
      <category>트러블슈팅</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/41</guid>
      <comments>https://youbin2.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 26 May 2025 14:33:32 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 스캔 효율화</title>
      <link>https://youbin2.tistory.com/40</link>
      <description>&lt;h1&gt;인덱스 스캔 효율화 과정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 스캔 효율성은 인덱스 칼럼을 등치(=) 조건으로 사용할 때 가장 좋다. 만약 인덱스 칼럼 중 일부가 등치 조건이 아니더라도 그 칼럼이 뒤쪽 칼럼일 경우에는 비효율이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런지 내가 실제 업무에서 인덱스 스캔 효율화를 한 과정을 통해 살펴보도록 하겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 테이블 인덱스 개선 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에서 새로운 로그 테이블을 만들었고, 그 로그 테이블에서 PK 인덱스만 있었다. PK 인덱스는 다음과 같이 설정되었다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;..., LOG_DT(로그 날짜), LOG_HMS(로그 시간), ...
&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;로그 테이블에서 주로 사용되는 쿼리를 알아보니 처리일자를 등치 조건(=), 처리시간을 BETWEEN 조건으로 사용하고 있었다. 또한 PK에 없는 GUID라는 필드는 IN 조건으로 활용되고 있었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;   select
        ...
    from
        ...
    where
		    ...
        LOG_DT=? 
        and (
            LOG_HMS between ? and ?
        ) 
        and (
            GUID in (
                ? , ? , ?
            )
        ) 
	    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서 인덱스를 최대로 활용할 수 있는 인덱스는 무엇일까? 다음은 새로운 인덱스 후보이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;hellip;, LOG_DT, LOG_HMS, GUID, &amp;hellip;&lt;/li&gt;
&lt;li&gt;&amp;hellip;, LOG_DT, GUID, LOG_HMS, &amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 인덱스를 인덱스 1, 두 번째 인덱스를 인덱스 2라고 부르겠다. 인덱스 1과, 인덱스 2를 생성 후 실험을 진행했다.&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;테스트 결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 25만 개의 로그를 생성 후 테스트 해봤다. 대부분의 RDB의 경우 메모리에 값을 캐시 한다. 따라서 정확한 테스트를 위해 쿼리 실행마다 캐시를 비워주었다. 쿼리를 작성 후 실행 계획을 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 1 결과&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;53&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BcI67/btsNuIpJnZU/OY8d0LQrkDhzxzAXGjMoP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BcI67/btsNuIpJnZU/OY8d0LQrkDhzxzAXGjMoP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BcI67/btsNuIpJnZU/OY8d0LQrkDhzxzAXGjMoP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBcI67%2FbtsNuIpJnZU%2FOY8d0LQrkDhzxzAXGjMoP1%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;246&quot; height=&quot;53&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;53&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rows : 185,626&lt;/li&gt;
&lt;li&gt;filtered : 50&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;185,626건을 스캔하고 그중 50%가 최종 결과로 넘어간다는 뜻이다. 조회시간은 약 0.05s&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 결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNCKPQ/btsNtSM9wR4/3EbpO5GBQ8wxDQTvZ9PLOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNCKPQ/btsNtSM9wR4/3EbpO5GBQ8wxDQTvZ9PLOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNCKPQ/btsNtSM9wR4/3EbpO5GBQ8wxDQTvZ9PLOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNCKPQ%2FbtsNtSM9wR4%2F3EbpO5GBQ8wxDQTvZ9PLOK%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;365&quot; height=&quot;48&quot; data-origin-width=&quot;365&quot; data-origin-height=&quot;48&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rows : 216&lt;/li&gt;
&lt;li&gt;filtered : 100&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;216건을 스캔하고 그중 100% 전부 최종 결과로 넘어간다. 조회시간은 약 0.005이다. 왜 이런 결과가 나왔을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 스캔 효율성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 선행 칼럼이 조건절에 없거나 &amp;lsquo;=&amp;rsquo; 조건이 아니면 인덱스 스캔 과정에서 비효율이 발생한다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt; 선행 칼럼, 후행 칼럼&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;선행 칼럼&amp;rsquo;은 어떤 칼럼보다 &amp;lsquo;상대적으로 앞쪽&amp;rsquo;에 놓인 칼럼을 지칭할 때 사용하겠다. 반대로 &amp;lsquo;후행 칼럼&amp;rsquo;은 &amp;lsquo;상대적으로 뒤쪽&amp;rsquo;에 놓인 칼럼을 지칭할 때 사용하겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 순서대로 인덱스가 설정되었다면 LOG_DT는 선행 칼럼, LOG_HMS는 후행 칼럼이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;..., LOG_DT(로그 날짜), LOG_HMS(로그 시간), ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 선행 칼럼이 조건절에 없거나 &amp;lsquo;=&amp;rsquo; 조건이 아니면 인덱스 스캔 과정에서 비효율이 발생하는 이유를 알기 위해 다음 예시를 보자. 4 문자를 잘라 테이블 칼럼에 각각 저장하고 [C1 + C2 + C3 + C4] 순서대로 인덱스를 생성했다.&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;&lt;span data-token-index=&quot;0&quot;&gt;[인덱스 테이블]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvCRuH/btsNunNhY4L/rT5Vxsm8JiQJQG78BtEWI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvCRuH/btsNunNhY4L/rT5Vxsm8JiQJQG78BtEWI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvCRuH/btsNunNhY4L/rT5Vxsm8JiQJQG78BtEWI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvCRuH%2FbtsNunNhY4L%2FrT5Vxsm8JiQJQG78BtEWI1%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;1404&quot; height=&quot;848&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 1.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테이블에서 &amp;lsquo;성능검&amp;rsquo;으로 시작하는 레코드를 검색하려면 어디서 스캔을 시작하고 멈춰야 될까?&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt; where c1 = '성'
 and c2 = '능'
 and c3 = '검'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;성능검&amp;rsquo;으로 시작하는 레코드를 검색할 때 인덱스 수직적 탐색을 통해 &amp;lsquo;성능검사&amp;rsquo; 레코드로 찾아간다. 거기서부터 스캔을 시작해 &amp;lsquo;성능계수&amp;rsquo;까지 총 3개의 레코드를 스캔하고 멈춘다. 두 건을 얻기 위해&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;1404&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqzLP8/btsNuoyD8xX/lBkpDvo9ni1AkRKmrgF2H1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqzLP8/btsNuoyD8xX/lBkpDvo9ni1AkRKmrgF2H1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqzLP8/btsNuoyD8xX/lBkpDvo9ni1AkRKmrgF2H1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqzLP8%2FbtsNuoyD8xX%2FlBkpDvo9ni1AkRKmrgF2H1%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;1404&quot; height=&quot;856&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 2.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;성능&amp;rsquo;으로 시작하고 네 번째 칼럼이 &amp;lsquo;선&amp;rsquo;인 레코드를 검색하면 어디서 스캔을 시작하고 어디서 멈출까? 쿼리문은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt; where c1 = '성'
 and c2 = '능'
 and c4 = '선'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 질문은 &amp;lsquo;성능&amp;rsquo;으로 시작하고 네 번째 칼럼이 &amp;lsquo;선&amp;rsquo;인 레코드를 검색할 때 아래처럼 &amp;lsquo;성능&amp;rsquo;으로 시작하는 레코드를 모두 스캔해야 한다. 결과는 똑같이 두 건이지만, 예시 1보다 훨씬 더 많은 인덱스 레코드를 스캔해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnXTmH/btsNtNF2Enz/nvOfiE6q1zqVGAXuIeF8gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnXTmH/btsNtNF2Enz/nvOfiE6q1zqVGAXuIeF8gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnXTmH/btsNtNF2Enz/nvOfiE6q1zqVGAXuIeF8gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnXTmH%2FbtsNtNF2Enz%2FnvOfiE6q1zqVGAXuIeF8gk%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;1404&quot; height=&quot;856&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 조건과 필터 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 액세스 조건&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 스캔 범위를 결정하는 조건절&lt;/li&gt;
&lt;li&gt;인덱스 수직적 탐색을 통해 스캔 시작점을 결정하는데 영향 미친다.&lt;/li&gt;
&lt;li&gt;인덱스 리프 블록 스캔하다 어디서 멈출지 결정 미치는 조건절&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 필터 조건&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블로 액세스 할지 결정하는 조건절&lt;/li&gt;
&lt;li&gt;쿠리 수행 다음 단계로 전달하거나 최종 결과집합에 포함할지 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 1번 예제에서 C1, C2, C3가 모두 인덱스 액세스 조건이었다. 2번 에제는 C1, C2가 인덱스 액세스 조건이고, C4는 인덱스 필터&amp;nbsp;조건이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 연산자 종류와 칼럼 순서에 따른 군집성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에는 &amp;lsquo;같은 값&amp;rsquo;을 갖는 레코드들이 군집해 있다. &amp;lsquo;같은 값&amp;rsquo;을 찾을 때 &amp;lsquo;=&amp;rsquo; 연산자를 사용하므로 인덱스 칼럼을 앞쪽부터 누락 없이 &amp;lsquo;=&amp;rsquo; 연산자로 조회하면 조건절을 만족하는 레코드는 모두 모여 있다. 인덱스 칼럼 중 어느 하나를 누락하거나 &amp;lsquo;=&amp;rsquo; 조건이 아닌 연산자로 조회하면 조건절 만족하는 레코드가 흩어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 인덱스 테이블이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1997&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVPWl4/btsNtejY8Wj/XNl7XdGuzyN2ucpaFcfuK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVPWl4/btsNtejY8Wj/XNl7XdGuzyN2ucpaFcfuK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVPWl4/btsNtejY8Wj/XNl7XdGuzyN2ucpaFcfuK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVPWl4%2FbtsNtejY8Wj%2FXNl7XdGuzyN2ucpaFcfuK0%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;1997&quot; height=&quot;799&quot; data-origin-width=&quot;1997&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 조건절로 인덱스 구성 칼럼을 모두 &amp;lsquo;=&amp;rsquo; 조건으로 비교할 때 조건을 만족하는 레코드들이 5~7번까지 모여 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;where C1 = 1
and C2 = 'A'
and C3 = '나'
and C4 = 'a'
&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;이와 달리 칼럼 중간에 범위검색 조건일 때는 다른 결과를 가져온다. 다음 조건절처럼 세 번째 칼럼 C3가 범위검색 조건인 경우는 C1부터 C3까지 세 조건을 만족하는 인덱스 레코드는 모여 있지만(2~12번), C4 조건까지 만족하는 레코드는 흩어지게 된다.(2, 3, 5, 6, 7, 11번)&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;where C1 = 1
and C2 = 'A'
and C3 between '가' and '다'
and C4 = 'a'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선행 칼럼이 모두 &amp;lsquo;=&amp;rsquo; 조건인 상태에서 첫 번째 나타나는 범위검색 조건까지만 만족하는 인덱스 레코드는 모두 연속해서 모여 있지만, 그 이하 조건까지 만족하는 레코드는 비교 연산자 종류에 상관없이 흩어진다는 규칙을 도출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 인덱스 스캔 범위를 결정하는 것은 인덱스 액세스 조건이고, 선행 칼럼이 &amp;lsquo;=&amp;rsquo; 조건인 상태에서 첫 번째 나타나는 범위검색 조건이 인덱스 스캔 범위를 결정한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BETWEEN을 IN-List로 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위검색 칼럼이 갖는 문제점을 어떻게 해결할까? 범위검색 칼럼이 맨 뒤로 가도록 인덱스 순서를 변경하면 좋겠지만 운영 시스템에서 쉽지 않다. 이럴 때 BETWEEN 조건을 IN-List로 바꿔주면 효과를 얻을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 1.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[인터넷 매물 + 아파트시세 코드 + 평형 + 평형 타입] 순서대로 인덱스를 설정 후 다음 BETWEEN 조건을 검색했다고 가정해 보자.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;select *
from 매물아파트매매
where 아파트시세코드='A01011350900056'
and 평형 '59'
and 평형타입 = 'A'
and 인터넷매물 between '1' and '3'
order by 입력일 desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 선두 칼럼 인터넷매물에 BETWEEN 연산자를 사용하면 나머지 조건(아파트시세코드, 평형, 평형타입)이 뿔뿔이 흩어지게 된다. 따라서 조건을 만족하지 않는 레코드까지 스캔하고서 버리는 비효율이 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1637&quot; data-origin-height=&quot;1370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M3ZBr/btsNtc0MQqF/iADITAM17O7wgRokCH92OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M3ZBr/btsNtc0MQqF/iADITAM17O7wgRokCH92OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M3ZBr/btsNtc0MQqF/iADITAM17O7wgRokCH92OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM3ZBr%2FbtsNtc0MQqF%2FiADITAM17O7wgRokCH92OK%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;1637&quot; height=&quot;1370&quot; data-origin-width=&quot;1637&quot; data-origin-height=&quot;1370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 2.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BETWEEN 조건절을 아래와 같이 IN-List로 바꿔보자.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;select *
from 매물아파트매매
where 인터넷매물 in ('1', '2', '3')
and 아파트시세코드='A01011350900056'
and 평형 = '59'
and 평형타입 = 'A'
order by 입력일 desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;1389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ChpqJ/btsNtLVJ6qM/PbKz6dMbRCAUewN6o4BBCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ChpqJ/btsNtLVJ6qM/PbKz6dMbRCAUewN6o4BBCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ChpqJ/btsNtLVJ6qM/PbKz6dMbRCAUewN6o4BBCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FChpqJ%2FbtsNtLVJ6qM%2FPbKz6dMbRCAUewN6o4BBCK%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;1907&quot; height=&quot;1389&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;1389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 인덱스 수직 탐색이 3번 발생한다. 실행 계획을 보면 INLIST ITERATOR가 발생하는 것을 알 수 있다. IN-List 개수만큼 아파트매물이 &amp;lsquo;1&amp;rsquo;인 경우 &amp;lsquo;2&amp;rsquo;인 경우 &amp;lsquo;3&amp;rsquo;인 경우 UNION ALL 브랜치가 생성되고 각 브랜치마다 모든 칼럼을 &amp;lsquo;=&amp;rsquo; 조건으로 검색하므로 선두 칼럼이 BETWEEN을 사용할 때와 같은 비효율이 사라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BETWEEN 조건을 IN-List 조건으로 전환 시 주의할 점은 IN-List 개수가 많지 않아야 한다. IN-List 개수가 많으면 수직 탐색 비용이 많이 발생한다. 그러면 BETWEEN 조건 때문에 리프 블록을 스캔하는 비효율보다 IN-List 개수만큼 브랜치 블록을 탐색하는 비효율이 클 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 인덱스 스캔 과정에서 선택되는 레코드들이 서로 멀리 떨어져 있을 때만 유용하다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;where 고객등급 between 'C' and 'D'
and 고객번호 = 123
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 [고객등급 + 고객번호] 순으로 구성한 인덱스에서 고객번호 = 123 조건을 만족하는 레코드가 멀리 떨어져 있을 때만 BETWEEN 조건을 IN-List로 전환하는 기법이 유용하다. BETWEEN 조건 때문에 인덱스를 비효율적으로 스캔하더라도 블록 IO 측면에서 소량에 그치는 경우가 많다. 인덱스 리프 블록에는 테이블 블록과 달리 많은 레코드가 담기기 때문이다. IN-List 개수가 많아질수록 수직적 탐색 과정에서 많은 블록을 읽게 되므로 성능이 안 좋아질 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 로그 데이터 쿼리를 보겠다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;   select
        ...
    from
        ...
    where
		    ...
        LOG_DT=? 
        and (
            LOG_HMS between ? and ?
        ) 
        and (
            GUID in (
                ? , ? , ?
            )
        ) 
	    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LOG_DT는 등치(=) 조건이고, LOG_HMS는 범위(BETWEEN) 조건이다. 따라서 LOG_DT가 LOG_HMS보다 인덱스 순서가 앞서야 한다. 마찬가지로 인덱스에서 GUID도 LOG_HMS보다 앞서야 한다. GUID는 범위 조건이 아닌 In-List조건이기 때문이다. BETWEEN을 사용하는 LOG_HMS는 인덱스에 맨 뒤쪽에 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001975837&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://product.kyobobook.co.kr/detail/S000001975837&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1745227189763&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;친절한 SQL 튜닝 | 조시형 - 교보문고&quot; data-og-description=&quot;친절한 SQL 튜닝 | 책 제목은 필자가 애청하는 라디오 프로그램 &amp;lsquo;손에 잡히는 경제&amp;rsquo; 중 &amp;lsquo;친절한 경제&amp;rsquo;라는 코너에서 착안했다. 어려운 경제 이슈를 일반인 눈높이에 맞게 풀어서 설명해 주는&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001975837&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000001975837&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KrR0U/hyYFzP4YO2/OeGh7Y6wEUp1DL2zV1H081/img.jpg?width=458&amp;amp;height=602&amp;amp;face=0_0_458_602,https://scrap.kakaocdn.net/dn/csbctQ/hyYH6zIFkU/fLL1QrezdXTuqxh9NnKBJ0/img.jpg?width=458&amp;amp;height=602&amp;amp;face=0_0_458_602,https://scrap.kakaocdn.net/dn/casYsr/hyYH9b8N51/h6iEebMsHIKX2UvwwDMHi0/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001975837&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001975837&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KrR0U/hyYFzP4YO2/OeGh7Y6wEUp1DL2zV1H081/img.jpg?width=458&amp;amp;height=602&amp;amp;face=0_0_458_602,https://scrap.kakaocdn.net/dn/csbctQ/hyYH6zIFkU/fLL1QrezdXTuqxh9NnKBJ0/img.jpg?width=458&amp;amp;height=602&amp;amp;face=0_0_458_602,https://scrap.kakaocdn.net/dn/casYsr/hyYH9b8N51/h6iEebMsHIKX2UvwwDMHi0/img.png?width=335&amp;amp;height=335&amp;amp;face=0_0_335_335');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;친절한 SQL 튜닝 | 조시형 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;친절한 SQL 튜닝 | 책 제목은 필자가 애청하는 라디오 프로그램 &amp;lsquo;손에 잡히는 경제&amp;rsquo; 중 &amp;lsquo;친절한 경제&amp;rsquo;라는 코너에서 착안했다. 어려운 경제 이슈를 일반인 눈높이에 맞게 풀어서 설명해 주는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>데이터베이스</category>
      <category>데이터베이스 튜닝</category>
      <category>디비</category>
      <category>디비 범위 연산</category>
      <category>범위 연산</category>
      <category>선행칼럼</category>
      <category>인덱스</category>
      <category>인덱스 스캔 효율화</category>
      <category>인덱스 효율화</category>
      <category>튜닝</category>
      <category>후행칼럼</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/40</guid>
      <comments>https://youbin2.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 21 Apr 2025 18:20:03 +0900</pubDate>
    </item>
    <item>
      <title>리팩터링</title>
      <link>https://youbin2.tistory.com/39</link>
      <description>&lt;h1&gt;리팩터링&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001810241&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://product.kyobobook.co.kr/detail/S000001810241&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744296438488&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;리팩터링 | 마틴 파울러 - 교보문고&quot; data-og-description=&quot;리팩터링 | 개발자가 선택한 프로그램 가치를 높이는 최고의 코드 관리 기술 마틴 파울러의 『리팩터링』이 새롭게 돌아왔다.지난 20년간 전 세계 프로그래머에게 리팩터링의 교본이었던 이 책&quot; data-og-host=&quot;product.kyobobook.co.kr&quot; data-og-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001810241&quot; data-og-url=&quot;https://product.kyobobook.co.kr/detail/S000001810241&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/d3bFW1/hyYEHNpwBb/5WZelcPQgKI0zOLee7hk41/img.jpg?width=458&amp;amp;height=564&amp;amp;face=0_0_458_564,https://scrap.kakaocdn.net/dn/rWuow/hyYEzPmnR8/YzA3oshK6LAWfU2mdII5bk/img.jpg?width=458&amp;amp;height=564&amp;amp;face=0_0_458_564&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001810241&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://product.kyobobook.co.kr/detail/S000001810241&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/d3bFW1/hyYEHNpwBb/5WZelcPQgKI0zOLee7hk41/img.jpg?width=458&amp;amp;height=564&amp;amp;face=0_0_458_564,https://scrap.kakaocdn.net/dn/rWuow/hyYEzPmnR8/YzA3oshK6LAWfU2mdII5bk/img.jpg?width=458&amp;amp;height=564&amp;amp;face=0_0_458_564');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리팩터링 | 마틴 파울러 - 교보문고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;리팩터링 | 개발자가 선택한 프로그램 가치를 높이는 최고의 코드 관리 기술 마틴 파울러의 『리팩터링』이 새롭게 돌아왔다.지난 20년간 전 세계 프로그래머에게 리팩터링의 교본이었던 이 책&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;product.kyobobook.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마틴 파울러의 리팩터링 2판을 읽어보았다. 책 1장은 리팩터링 예시를 설명하고 있다. 2장은 리팩터링이 무엇인지 왜 해야 하는지를 설명하고 있다. 3장은 리팩터링이 필요한 코드 스멜 부분을 설명하고 있으며 4장은 리팩터링에 필요한 테스트를 설명하고 있다. 나머지 부분은 리팩터링 기법을 설명한 카탈로그이다. 책의 저자도 카탈로그는 필요할 때마다 찾아보는 것을 권장하기 때문에 따로 정리하지 않았다. 1장부터 3장까지 내용을 정리해 보았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링 정의는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리팩터링 : 소프트웨어 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;겉보기 동작&amp;rsquo; 뜻이 애매해 보이는데 간단히 말하자면 리팩터링 전과 후의 코드가 똑같이 동작해야 한다는 뜻이다. 리팩터링의 목적은 코드의 기능이 똑같이 작동하지만 코드를 이해하고 수정하기 쉽게 만드는 것이다. 리팩터링은 프로그램의 성능이 개선되는 것과는 상관없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 하는 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 소프트웨어 설계가 좋아진다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩터링 하지 않으면 내부 아키텍처가 무너지기 쉽다.&lt;/li&gt;
&lt;li&gt;규칙적인 리팩터링은 코드 구조 지탱한다.&lt;/li&gt;
&lt;li&gt;코드 중복 제거와 같은 리팩터링은 설계 개선 작업에 중요한 역할 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 소프트웨어 이해가 쉬워진다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리팩터링은 코드가 잘 읽히게 도와준다&lt;/li&gt;
&lt;li&gt;코드의 목적, 의도를 명확하게 전달하도록 개선할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 버그를 쉽게 찾을 수 있다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 이해가 쉽다 &amp;rarr; 버그를 찾기 쉽다는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 프로그래밍 속도 높일 수 있다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 설계가 잘 된 소프트웨어는 새 기능과 버그를 쉽게 고칠 수 있다.&lt;/li&gt;
&lt;li&gt;설계가 좋을수록 빠른 기능이 누적될수록 빠른 개발이 가능하다.&lt;/li&gt;
&lt;li&gt;리팩터링의 목적은 개발 기간을 단축하는 것이다. 기능 추가 시간 줄이고 버그 수정 시간 줄여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 시기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능을 쉽게 추가하도록 하는 리팩터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에 기능을 새로 추가하기 전에 리팩터링을 권장한다. 기능 추가 전 코드의 구조를 바꾸면 다른 작업을 하기 쉬워질 만한 부분을 찾는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드를 이해하기 쉽게 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 의도를 명확하게 드러나도록 리팩터링 하는 것이 좋다. 조건부 로직 이상한지 살펴보고, 함수 이름 잘 지었는지 등등을 살펴보는 습관을 기르자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰레기 줍기 리팩터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정이 간단한 코드는 바로 리팩터링 하고, 오래 걸리는 코드는 메모만 남긴 후 하던 일을 끝내고 처리하자. 이를 쓰레기 줍기 리팩터링이라고 부른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수시로 하는 리팩터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링을 하려고 따로 일정을 잡지 않고 이뤄지는 리팩터링을 수시로 하는 리팩터링이라고 부른다. 기능 추가 시 새로운 코드를 작성해 넣는다고 생각하지만, 뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 &amp;lsquo;수정&amp;rsquo;하는 것이 기능을 빠르게 추가하는 방법이라고 생각한다. 즉 새 기능이 필요할 때마다 소프트웨어는 이를 반영하기 위해 수정된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오래 걸리는 리팩터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래 걸리는 대규모 리팩터링(ex 라이브러리 교체 작업, 컴포넌트 빼내는 작업 등등)을 팀 전체가 하는 것을 추천하지 않는다. 원하는 방향으로 조금씩 개선하는 방법을 추천한다. 예를 들어 라이브러리 교체 시 기존 것과 새것 모두 포용하는 인터페이스부터 마련한다. 기존 코드가 인터페이스를 호출하도록 만들면, 라이브러리 교체가 쉬워진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리팩터링 하지 말아야 할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 지저분해도 굳이 수정할 필요 없다면 리팩터링 하지 않는다. 내부 동작을 이해해야 할 시점에 리팩터링 하는 것을 권장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 개발 프로세스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 자가 테스트 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링은 동작이 깨지지 않아야 한다. 이를 위해 오류를 빨리 잡아야 하고, 테스트가 필요하다. 리팩터링을 위해 자가 테스트 코드를 마련해야 한다. 테스트 코드는 리팩터링을 돕고 새 기능 추가도 안전하게 진행하도록 도와준다. 견고한 테스트는 리팩터링 과정에서 생길 버그를 줄여준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 지속적 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 시 기능마다 브랜치를 따로 만드는 경우가 있다. 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터 브랜치로 통합이 어려워진다. 브랜치의 통합 주기를 짧게 관리해야 한다. 자주 마스터 브랜치와 통합하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 낮출 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링시에 각 팀원이 다른 사람의 작업을 방해하지 않아야 한다. 리팩터링 할 때 지속적 통합을 권장하는 이유이다. 지속적 통합을 통해 리팩터링 한 결과가 다른 팀원의 작업에 문제를 일으키면 즉시 알아낼 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드스멜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 코드를 리팩터링 해야 할지 알아야 한다. 책의 저자는 리팩터링이 필요한 코드들에 일정한 패턴이 있다고 설명하고 있으며 이를 코드 스멜이라고 부른다. 코드 스멜은 리팩터링 하면 해결할 수 있다고 말한다. 책의 저자가 설명한 코드 스멜을 요약해 보았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기이한 이름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수, 함수, 클래스명만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 절약할 수 있다. 이름이 애매하다면 설계에 문제가 숨어 있을 가능성이 높다. 이름을 장 정리해 코드를 잘 파악하도록 하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 중복 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합하자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class DuplicateCodeExample {

    // 주문 생성 시 공통으로 사용하는 로깅 코드
    public void createOrder(String orderId, double amount) {
        // 중복된 로깅 코드
        System.out.println(&quot;START: 주문 생성&quot;);
        System.out.println(&quot;Order ID: &quot; + orderId);
        System.out.println(&quot;Amount: &quot; + amount);
        System.out.println(&quot;END: 주문 생성&quot;);
        // 주문 생성 로직 ...
    }
    
    // 주문 취소 시도 동일한 로깅 코드가 중복됨
    public void cancelOrder(String orderId) {
        // 중복된 로깅 코드
        System.out.println(&quot;START: 주문 취소&quot;);
        System.out.println(&quot;Order ID: &quot; + orderId);
        System.out.println(&quot;END: 주문 취소&quot;);
        // 주문 취소 로직 ...
    }
    
    // 위의 로깅 코드를 별도의 메서드로 추출하여 중복 제거가 필요함
    public void log(String action, String orderId, Double amount) {
        System.out.println(&quot;START: &quot; + action);
        System.out.println(&quot;Order ID: &quot; + orderId);
        if(amount != null) {
            System.out.println(&quot;Amount: &quot; + amount);
        }
        System.out.println(&quot;END: &quot; + action);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 로깅이 중복된다면 별도의 메서드로 추출하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 긴 함수&lt;/h3&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;lsquo;의도&amp;rsquo;가 드러나게 짓는다.(how가 아닌 what으로 짓는다.) 함수 이름에 코드 목적을 드러내야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Order {
    private List&amp;lt;Item&amp;gt; items;

    public boolean isEligibleForFreeShipping() {
        int total = 0;
        for (Item item : items) {
            total += item.getPrice();
        }
        return total &amp;gt;= 50000;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 함수를 나누면 다음과 같아진다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Order {
    private List&amp;lt;Item&amp;gt; items;

    public boolean isEligibleForFreeShipping() {
        return totalAmount() &amp;gt;= freeShippingThreshold();
    }

    private int totalAmount() {
        return items.stream()
                    .mapToInt(Item::getPrice)
                    .sum();
    }

    private int freeShippingThreshold() {
        return 50000;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 긴 매개변수 목록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수 목록이 길어지면 이해하기 어려울 때가 많다. 매개변수를 객체로 만들거나 여러 함수를 클래스로 묶자.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 매개변수가 많다.
public void sendEmail(String to, String subject, String body, String from, String replyTo)

//다음과 같이 줄이자.
public void send(EmailMessage message)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 전역 데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 데이터는 코드베이스 어디에서든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 메커니즘이 없다. 대표적인 전역 데이터 형태는 전역 변수, 클래스 변수, 싱글톤이 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class AppConfig {
    public static String environment = &quot;dev&quot;;
}

...

if (AppConfig.environment.equals(&quot;prod&quot;)) {
    // 중요한 로직 실행
}

...

AppConfig.environment = &quot;prod&quot;; // 갑자기 환경이 바뀌면 문제가 발생할 수 있다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 가변 데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 변경하면 예상하지 못한 버그로 이어지는 경우가 종종 있다. 코드 다른 곳에서 다른 값을 사용한다는 생각을 하지 못한 채 수정하면 프로그램이 오작동한다. 함수형 언어를 쓴다면 데이터의 불변성을 보장하지만 대부분 프로그램 언어는 변수 값을 바꾸는 것을 지원한다. 앞에서 봤던 전역 데이터와 비슷한 경우이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 뒤엉킨 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤엉킨 변경이란 단일 책임 원칙이 지켜지지 않을 때 발생한다. 하나의 모듈이 서로 다른 이유로 인해 변경되는 일이 많을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터베이스가 추가될 때 함수 세 개를 바꿔야 하고, 금융 상품이 추가될 때마다 다른 함수 네 개를 바꿔야 하는 모듈이 발생했다면 뒤엉킨 변경이 발생했다는 뜻이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 산탄총 수술&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 산탄총 수술이 발생한다. 변경할 부분이 코드 전반에 퍼져있는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시를 보면 고객 관련 기능이 여러 곳에 흩어져 있다. 고객 코드를 변경해야 할 때 여러 클래스를 고쳐야 한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Customer {
    private String name;
    private String address;
    private String phoneNumber;

    public void printName() {
        System.out.println(&quot;Customer Name: &quot; + name);
    }

    public void printAddress() {
        System.out.println(&quot;Customer Address: &quot; + address);
    }

    public void printPhoneNumber() {
        System.out.println(&quot;Customer Phone: &quot; + phoneNumber);
    }
}

public class Invoice {
    private Customer customer;

    public void printInvoice() {
        System.out.println(&quot;INVOICE&quot;);
        customer.printName();
        customer.printAddress(); // 이곳도 수정 대상
        // ...
    }
}

public class ShippingLabel {
    private Customer customer;

    public void printLabel() {
        System.out.println(&quot;SHIPPING LABEL&quot;);
        customer.printName();    // 이곳도 수정 대상
        customer.printAddress(); // 이곳도 수정 대상
        // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 고객 관련 기능을 하나로 묶어야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class Customer {
    private String name;
    private String address;
    private String phoneNumber;

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getContactInfo() {
        return &quot;Customer Name: &quot; + name + &quot;\\nCustomer Phone: &quot; + phoneNumber;
    }

    public String getFullAddressLabel() {
        return &quot;Customer Address: &quot; + address;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer 클래스가 변경되어도 Invoice와 ShippingLabel 클래스는 변경하지 않아도 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Invoice {
    private Customer customer;

    public void printInvoice() {
        System.out.println(&quot;INVOICE&quot;);
        System.out.println(customer.getContactInfo());
        System.out.println(customer.getFullAddressLabel());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class ShippingLabel {
    private Customer customer;

    public void printLabel() {
        System.out.println(&quot;SHIPPING LABEL&quot;);
        System.out.println(customer.getContactInfo());
        System.out.println(customer.getFullAddressLabel());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 기능 편애&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 모듈에 있는 상호작용은 늘리고, 다른 모듈과의 상호작용은 최소로 줄여야 한다. 기능 편애는 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 많을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시를 보자. generateReport()는 CustomerReport 클래스에 있지만, 실제로는 Customer 객체의 필드를 거의 다 사용한다. 이 메서드는 사실상 Customer에 더 관심이 많다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class Customer {
    private String name;
    private String address;
    private int loyaltyPoints;

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public int getLoyaltyPoints() {
        return loyaltyPoints;
    }
}

public class CustomerReport {

    public String generateReport(Customer customer) {
        StringBuilder report = new StringBuilder();
        report.append(&quot;Name: &quot;).append(customer.getName()).append(&quot;\\n&quot;);
        report.append(&quot;Address: &quot;).append(customer.getAddress()).append(&quot;\\n&quot;);
        report.append(&quot;Loyalty Points: &quot;).append(customer.getLoyaltyPoints()).append(&quot;\\n&quot;);

        return report.toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 데이터 뭉치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 항목 서너 개가 항상 함께 뭉쳐 다닐 때 데이터 뭉치라고 부른다. 서로 항상 같이 다니는 데이터들이 여러 클래스나 메서드에 반복해서 나타날 때 발생한다. 몰려다니는 데이터는 따로 클래스를 추출하는 형태로 묶는 방법을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제를 보면 customerName, customerAddress, customerPhone이 항상 같이 호출된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Invoice {
    public void printInvoice(String customerName, String customerAddress, String customerPhone) {
        System.out.println(&quot;Invoice for: &quot; + customerName);
        System.out.println(&quot;Address: &quot; + customerAddress);
        System.out.println(&quot;Phone: &quot; + customerPhone);
    }
}

public class ShippingLabel {
    public void printLabel(String customerName, String customerAddress, String customerPhone) {
        System.out.println(&quot;Ship to: &quot; + customerName);
        System.out.println(customerAddress);
        System.out.println(&quot;Contact: &quot; + customerPhone);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 하나의 클래스로 묶어야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class CustomerInfo {
    private String name;
    private String address;
    private String phone;

    public CustomerInfo(String name, String address, String phone) {
        this.name = name;
        this.address = address;
        this.phone = phone;
    }

    public String getName() { return name; }
    public String getAddress() { return address; }
    public String getPhone() { return phone; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 기본형 집착&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 자료형(int, String, boolean 등)에 너무 의존하거나, 개념적으로 하나의 타입이 될 수 있는 값들을 그냥 기본형으로만 다루는 방법을 의미한다. 여러 String이나 int로 묶이는 값을 &lt;b&gt;클래스로 표현하지 않고&lt;/b&gt; 계속 기본형으로만 다루는 코드 스멜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 보자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class User {
    private String name;
    private String email;
    private String phone;

    public User(String name, String email, String phone) {
        this.name = name;
        this.email = email;
        this.phone = phone;
    }

    public boolean hasValidEmail() {
        return email.contains(&quot;@&quot;);
    }

    public boolean hasValidPhone() {
        return phone.length() == 10;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;email, phone을 기본형(String)으로 만들었다. 따라서 검증 로직이 User 클래스 안에 있게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12. 반복되는 switch문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무조건 switch문이나 if문이 있다고 다형성을 변경하는 것을 추천하지 않는다. switch문과 if문이 반복해서 나타날 때 리팩터링을 고민해 보자. 중복된 switch문은 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 한다. 이럴 때 객체지향의 다형성은 반복된 switch문을 개선할 수 있게 해 준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13. 반복문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문을 사용할 때 map, filter와 같은 컬랙션을 사용해 보자. 로직을 스트림으로 처리 시 객체가 어떻게 처리되는지 이해하기 쉬워진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14. 성의 없는 요소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어가 제공하는 함수, 클래스, 인터페이스를 의미 없이 사용하는 코드를 성의 없는 요소라고 부른다. 클래스 안에 메서드가 1개만 있거나, 본문 코드를 그대로 쓴 것과 다를 바 없는 함수도 있다. 이런 의미 없는 프로그램 요소는 제거하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;15. 추측성 일반화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언젠간 쓸지 몰라서 만들어 놓은 코드를 추측성 일반화라고 부른다. 확장성을 위한 파라미터지만 항상 null이라던가 사용되지 않는 추상 클래스나 인터페이스를 의미한다. 성의 없는 요소와 마찬가지로 제거하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16. 임시 필드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 상황에서만 값이 설정되고 그 외 상황에 null로 설정되는 필드를 임시 필드라고 부른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;17. 메시지 체인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다. getter가 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드를 의미한다. 다음이 대표적인 예시이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;managerName = person.getDepartment().getManager().getName();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18. 중개자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 클래스가 다른 객체의 메서드나 데이터를 대신 호출해 주는 역할만 할 때 그 클래스는 중개자 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Person {
    private Department department;

    public String getManager() {
        return department.getManager();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 Person은 Department의 중개자 역할을 한다. 보통 사용 시 문제가 되지 않지만 지나치면 문제가 된다. 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하면 문제가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;19. 내부자 거래&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 모듈이 서로의 내부 구현에 지나치게 의존하는 경우를 내부자 거래라고 부른다. 이런 경우 모듈 사이에 결합도가 높아진다. 결합도를 낮추고 투명하게 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 상속 구조에서 부모 자식 사이에 내부자 거래가 발생하는 경우가 있다. 다음은 예제 코드이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Employee {
    protected String name;
    protected double baseSalary;

    public Employee(String name, double baseSalary) {
        this.name = name;
        this.baseSalary = baseSalary;
    }

    public double calculateSalary() {
        return baseSalary;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class Salesman extends Employee {
    private double commission;

    public Salesman(String name, double baseSalary, double commission) {
        super(name, baseSalary);
        this.commission = commission;
    }

    @Override
    public double calculateSalary() {
        // 내부자 거래: 자식이 부모 필드에 직접 의존
        return baseSalary + commission;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Salesman이 baseSalary를 직접 참조하고 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;20. 거대한 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 클래스가 많은 일을 하다 보면 필드 수가 늘어난다. 클래스에 필드가 많으면 중복 코드가 생기기 쉽다. 이런 경우 클래스를 추출하여 일부 필드를 따로 묶는 것을 권장한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;21. 서로 다른 인터페이스의 대안 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 간의 인터페이스와 함수가 중복될 때 발생하는 코드 스멜이다. 공통되는 함수나 인터페이스를 슈퍼 클래스로 추출하는 방법을 권장한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;22. 데이터 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 클래스란 데이터 필드와 getter/setter 메서드로만 구성된 클래스를 뜻한다. 변경하면 안 되는 필드는 setter를 제거한다. 다른 클래스에서 데이터 클래스의 getter/setter를 사용하는 메서드를 찾아서 데이터 클래스로 옮길 수 있는지 고민하는 게 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;23. 상속 포기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향 설계에서 상속을 잘못 사용했을 때 자주 나타나는 문제이다. 서브 클래스가 부모의 동작은 필요로 하지만 인터페이스는 따르지 않고 싶은 경우가 대표적인 예이다. 이럴 때 상속을 포기하고 위임(composition)을 사용하는 방법을 권장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h1&gt;리팩터링 예시&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩터링 책 1장에 리팩터링 예시가 나와있다. 코드가 js로 되어 있어 이 부분을 java로 다시 구현해 보았다. 리팩터링 예시에 나온 요구사항은 다음과 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연극 공연 데이터를 바탕으로 고객에게 청구서를 생성하는 프로그램이다.&lt;/li&gt;
&lt;li&gt;특정 고객의 청구 내역을 출력하는 statement() 메서드를 제공한다.&lt;/li&gt;
&lt;li&gt;연극 유형별 가격 계산을 해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비극(&quot;tragedy&quot;)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본요금: 40,000원&lt;/li&gt;
&lt;li&gt;관객이 30명 초과 시, 초과 인원당 1,000원 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;희극(&quot;comedy&quot;)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본요금: 30,000원&lt;/li&gt;
&lt;li&gt;관객이 20명 초과 시, 10,000원 추가 + 초과 인원당 500원 추가&lt;/li&gt;
&lt;li&gt;추가로, 모든 관객 수에 대해 1인당 300원 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;관객이 30명을 초과하면 초과 인원만큼 포인트 추가.&lt;/li&gt;
&lt;li&gt;희극(&quot;comedy&quot;)의 경우, 추가 보너스 포인트로 (관객 수 / 5)의 내림값을 추가.&lt;/li&gt;
&lt;li&gt;결과 출력 포맷
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연극 제목과 해당 공연의 총비용 (100으로 나누어 원화 변환)&lt;/li&gt;
&lt;li&gt;공연별 관객 수&lt;/li&gt;
&lt;li&gt;총비용 합계&lt;/li&gt;
&lt;li&gt;적립 포인트 합계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 요구사항을 코드로 구현해 보았다. Play와 Invoice를 담을 객체는 다음과 같이 설정했다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Getter
@Setter
public class Play {
    public Play(String name, String type) {
        this.name = name;
        this.type = type;
    }

    private String name;
    private String type;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Getter
@Setter
public class Invoice {
    private String customer;
    private List&amp;lt;Performance&amp;gt; performances;

    public Invoice(String customer, List&amp;lt;Performance&amp;gt; performances) {
        this.customer = customer;
        this.performances = performances;
    }
}
&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;청구서를 출력하는 statement 함수는 다음과 같이 작성했다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        String result = main.statement(main.getInvoice(), main.getPlays());
        System.out.println(result);
    }

    public  String statement(Invoice invoice, Map&amp;lt;String, Play&amp;gt; plays) {
        double totalAmount = 0.d;
        double volumeCredits = 0.d;
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);

        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \\n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            Play play = plays.get(performance.getPlayId());
            double thisAmount = 0.d;

            switch (play.getType()) {
                case &quot;tragedy&quot;:
                    thisAmount = 40000.d;
                    if(performance.getAudience() &amp;gt; 30) {
                        thisAmount += 1000 * (performance.getAudience() - 30);
                    }
                    break;
                case &quot;comedy&quot;:
                    thisAmount = 30000.d;
                    if(performance.getAudience() &amp;gt; 20) {
                        thisAmount += 10000 + 500 * (performance.getAudience() - 20);
                    }
                    thisAmount += 300 * performance.getAudience();
                    break;
                default:
                    throw new RuntimeException(&quot;알 수 없는 장르:&quot; + play.getType());
            }

            volumeCredits += Math.max(performance.getAudience() - 30, 0);
            if(play.getType().equals(&quot;comedy&quot;)) {
                volumeCredits += Math.floor((double) performance.getAudience() / 5);
            }

            result.append(play.getName()).append(&quot;: &quot;).append(numberFormat.format(thisAmount / 100)).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \\n&quot;);
            totalAmount += thisAmount;
        }

        result.append(&quot;총액:&quot;).append(numberFormat.format(totalAmount / 100)).append('\\n');
        result.append(&quot;적립 포인트:&quot;).append(volumeCredits).append(&quot;점 \\n&quot;);

        return result.toString();
    }

    private Invoice getInvoice() {
        List&amp;lt;Performance&amp;gt; performances = Arrays.asList(
                new Performance(&quot;hamlet&quot;, 55),
                new Performance(&quot;as-like&quot;, 35),
                new Performance(&quot;othello&quot;, 40)
        );

        return new Invoice(&quot;BigCo&quot;, performances);
    }

    private Map&amp;lt;String, Play&amp;gt; getPlays() {
        Map&amp;lt;String, Play&amp;gt; plays = new HashMap&amp;lt;&amp;gt;();
        plays.put(&quot;hamlet&quot;, new Play(&quot;Hamlet&quot;, &quot;tragedy&quot;));
        plays.put(&quot;as-like&quot;, new Play(&quot;As You Like It&quot;, &quot;comedy&quot;));
        plays.put(&quot;othello&quot;, new Play(&quot;Othello&quot;, &quot;tragedy&quot;));

        return plays;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement() 함수에 모든 로직이 다 들어가 있다. 요구사항이 추가되거나 변경되면 statement() 함수를 수정해야 한다. statement() 함수의 복잡도가 크기 때문에 복잡한 작업이 될 것이다. 위 함수를 리팩터링 해보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 - statement() 함수 쪼개기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작에 따라 함수를 추출해 보겠다. 동작에 따라 함수를 추출한다는 게 무슨 뜻일까? statement() 매서드 안에 switch 문을 보면 공연에 대한 요금을 계산하고 있다. 즉 switch문은 공연에 대한 요금을 계산하는 동작을 하고 있다. 이를 amountFor(String aPerformance)라는 함수로 추출할 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; 		//함수 추출
    private double amountFor(Performance aPerformance) {
        double thisAmount = 0.d;

        switch (playFor(aPerformance).getType()) {
            case &quot;tragedy&quot;:
                thisAmount = 40000.d;
                if(aPerformance.getAudience() &amp;gt; 30) {
                    thisAmount += 1000 * (aPerformance.getAudience() - 30);
                }
                break;
            case &quot;comedy&quot;:
                thisAmount = 30000.d;
                if(aPerformance.getAudience() &amp;gt; 20) {
                    thisAmount += 10000 + 500 * (aPerformance.getAudience() - 20);
                }
                thisAmount += 300 * aPerformance.getAudience();
                break;
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + playFor(aPerformance).getType());
        }
        return thisAmount;
    }

&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;위와 같이 코드의 기능에 따라 함수를 추출할 것이다. statement() 메서드 안에 기능은 다음과 같다. 달러에 따른 가격 계산, 가격 총합 계산, 적립액 계산이다. 추출한 후 코드는 다음과 같아진다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class Main {
    public String statement(Invoice invoice, Map&amp;lt;String, Play&amp;gt; plays) {
        double totalAmount = 0.d;
        double volumeCredits = 0.d;

        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \\n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            volumeCredits += volumeCreditsFor(performance);
            //함수 인라인
            result.append(playFor(performance).getName()).append(&quot;: &quot;).append(usd(amountFor(performance))).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \\n&quot;);
            totalAmount += amountFor(performance);
        }

        result.append(&quot;총액:&quot;).append(usd(totalAmount)).append('\\n');
        result.append(&quot;적립 포인트:&quot;).append(volumeCredits).append(&quot;점 \\n&quot;);

        return result.toString();
    }

    //함수 추출. 달러 가격 계산
    private String usd(double aNumber) {
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
        return numberFormat.format(aNumber/100);
    }

    //함수 추출. 적립액 계산
    private double volumeCreditsFor(Performance performance) {
        double result = 0.d;
        result += Math.max(performance.getAudience() - 30, 0);

        if(playFor(performance).getType().equals(&quot;comedy&quot;)) {
            result += Math.floor((double) performance.getAudience() / 5);
        }
        return result;
    }

    //함수 추출. performance에서 play 객체 추출
    private Play playFor(Performance performance) {
        return plays.get(performance.getPlayId());
    }

    //함수 추출. 총합 계산
    private double amountFor(Performance aPerformance) {
        double thisAmount = 0.d;

        switch (playFor(aPerformance).getType()) {
            case &quot;tragedy&quot;:
                thisAmount = 40000.d;
                if(aPerformance.getAudience() &amp;gt; 30) {
                    thisAmount += 1000 * (aPerformance.getAudience() - 30);
                }
                break;
            case &quot;comedy&quot;:
                thisAmount = 30000.d;
                if(aPerformance.getAudience() &amp;gt; 20) {
                    thisAmount += 10000 + 500 * (aPerformance.getAudience() - 20);
                }
                thisAmount += 300 * aPerformance.getAudience();
                break;
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + playFor(aPerformance).getType());
        }
        return thisAmount;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 말한 기능에 따라 usd, volumeCreditsFor, amountFor 함수가 생성되었다. 앞에서 말한 기능을 제외하고 playFor이라는 함수가 추가로 만들어졌다. playFor 함수를 보면 변수 대신에 사용된 것을 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// Play play = plays.get(performance.getPlayId());
// result.append(play.getName()) ... 
result.append(playFor(performance).getName()) ...
&lt;/code&gt;&lt;/pre&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;다음은 반복문 쪼개기를 알아보겠다. 하나의 for문 안에 volumeCredits와 totalAmount를 구하는 로직이 있다. 각자 다른 기능을 하고 있기 때문에 리팩터링시 쪼개주는 것이 좋다. totalAmound와 totalVolumeCredits라는 함수로 나눠보겠다. 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class Main {
    private Map&amp;lt;String, Play&amp;gt; plays;
    private Invoice invoice;

    public  String statement(Invoice invoice, Map&amp;lt;String, Play&amp;gt; plays) {
        double totalAmount = 0.d;

        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \\n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            //함수 인라인
            result.append(playFor(performance).getName()).append(&quot;: &quot;).append(usd(amountFor(performance))).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \\n&quot;);
        }

        result.append(&quot;총액:&quot;).append(usd(totalAmount())).append('\\n');
        result.append(&quot;적립 포인트:&quot;).append(totalVolumeCredits()).append(&quot;점 \\n&quot;);

        return result.toString();
    }

    private double totalAmount() {
        double totalAmount = 0;
        for(Performance performance : invoice.getPerformances()) {
            totalAmount += amountFor(performance);
        }
        return totalAmount;
    }

    private double totalVolumeCredits() {
        double volumeCredits = 0.d;
        for(Performance performance : invoice.getPerformances()) {
            volumeCredits += volumeCreditsFor(performance);
        }
        return volumeCredits;
    }
    
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번만 돌아도 되는 for문이 3번이나 돌게 된다. 성능이 떨어져 보일 수 있다. 책에서는 리팩터링 후 성능 개선 작업을 하라고 추천한다. 성능이 먼저인지 코드 가독성이 먼저인지는 개인적으로 고민해 볼 문제인 것 같다. 아무튼 리팩터링 작업 후 statement() 함수가 줄어든 것을 확인할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩터링 - 다형성 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건부 로직을 다형성으로 바꿔보겠다. 코드를 보면 amountFor()와 volumeCreditsFor() 메서드가 공연료와 적립 포인트를 계산한다. 이 두 함수를 전용 클래스로 따로 옮겨보겠다. 공연 관련 데이터를 계산하는 역할을 하는 PerformanceCalculator라는 함수를 생성하겠다. 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class PerformanceCalculator {
    public PerformanceCalculator(Performance performance, Play play) {
        this.performance = performance;
        this.play = play;
    }

    private Performance performance;
    private Play play;

    public double amount(Performance performance) {
        double thisAmount = 0.d;

        switch (play.getType()) {
            case &quot;tragedy&quot;:
                thisAmount = 40000.d;
                if(performance.getAudience() &amp;gt; 30) {
                    thisAmount += 1000 * (performance.getAudience() - 30);
                }
                break;
            case &quot;comedy&quot;:
                thisAmount = 30000.d;
                if(performance.getAudience() &amp;gt; 20) {
                    thisAmount += 10000 + 500 * (performance.getAudience() - 20);
                }
                thisAmount += 300 * performance.getAudience();
                break;
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + play.getType());
        }
        return thisAmount;
    }

    public double volumeCreditsFor(Performance performance) {
        double result = 0.d;
        result += Math.max(performance.getAudience() - 30, 0);

        if(play.getType().equals(&quot;comedy&quot;)) {
            result += Math.floor((double) performance.getAudience() / 5);
        }
        return result;
    }

}
&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;statement 함수는 다음과 같이 변경된다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public  String statement(Invoice invoice, Map&amp;lt;String, Play&amp;gt; plays) {
        double totalAmount = 0.d;

        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \\n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            performanceCalculator = new PerformanceCalculator(performance, playFor(performance));
            //함수 인라인
            result.append(playFor(performance).getName()).append(&quot;: &quot;).append(usd(performanceCalculator.amount(performance))).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \\n&quot;);
        }

        result.append(&quot;총액:&quot;).append(usd(totalAmount())).append('\\n');
        result.append(&quot;적립 포인트:&quot;).append(totalVolumeCredits()).append(&quot;점 \\n&quot;);

        return result.toString();
    }

    private double totalAmount() {
        double totalAmount = 0;
        for(Performance performance : invoice.getPerformances()) {
            totalAmount += performanceCalculator.amount(performance);
            //함수 인라인
        }
        return totalAmount;
    }

    private double totalVolumeCredits() {
        double volumeCredits = 0.d;
        for(Performance performance : invoice.getPerformances()) {
            volumeCredits += performanceCalculator.volumeCreditsFor(performance);
            //함수 인라인
        }
        return volumeCredits;
    }

    //함수 추출
    private String usd(double aNumber) {
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
        return numberFormat.format(aNumber/100);
    }

    //함수 추출
    private Play playFor(Performance performance) {
        return plays.get(performance.getPlayId());
    }
&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;PerformanceCalculator를 보면 play의 타입에 따라 amount메서드와 volumeCreditsFor가 달라질 수 있다. 이를 피하기 위해 다형성을 활용하겠다. PerformanceCalculator를 상속받는 서브클래스를 준비하고 팩토리 함수를 활용해 서브 클래스를 생성하도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PerformanceCalculator를 인터페이스로 만든다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface PerformanceCalculator {
    double amount(Performance performance);
    double volumeCreditsFor(Performance performance);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PerformanceCalculator를 상속받은 TragedyCalculator와 ComedyCalculator를 만든다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class ComedyCalculator implements PerformanceCalculator {
    public ComedyCalculator(Performance performance, Play play) {
        this.performance = performance;
        this.play = play;
    }

    private Performance performance;
    private Play play;

    @Override
    public double amount(Performance performance) {
        double thisAmount = 30000.d;
        if(performance.getAudience() &amp;gt; 20) {
            thisAmount += 10000 + 500 * (performance.getAudience() - 20);
        }
        thisAmount += 300 * performance.getAudience();
        return thisAmount;
    }

    @Override
    public double volumeCreditsFor(Performance performance) {
        double result = Math.max(performance.getAudience() - 30, 0);
        result += Math.floor((double) performance.getAudience() / 5);
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class TragedyCalculator implements PerformanceCalculator {
    public TragedyCalculator(Performance performance, Play play) {
        this.performance = performance;
        this.play = play;
    }

    private Performance performance;
    private Play play;

    public double amount(Performance performance) {
        double thisAmount = 40000.d;
        if(performance.getAudience() &amp;gt; 30) {
            thisAmount += 1000 * (performance.getAudience() - 30);
        }
        return thisAmount;
    }

    public double volumeCreditsFor(Performance performance) {
        return Math.max(performance.getAudience() - 30, 0);
    }
}
&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;이전 amount에서 switch문에 따라 해야 하는 일이 달라졌다. 새로운 장르가 추가되면 amount를 추가해야 했다. 하지만 다형성을 활용해서 그럴 필요가 없어졌다. PerformanceCalculator를 상속받은 새로운 클래스를 만들어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PerformanceCalculator의 서브클래스를 생성하는 생성 팩토리 메서드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private PerformanceCalculator createPerformanceCalculator(Performance performance, Play play) {
        switch (play.getType()) {
            case &quot;tragedy&quot;:
                return new TragedyCalculator(performance, play);
            case &quot;comedy&quot;:
                return new ComedyCalculator(performance, play);
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + play.getType());
        }
    }
&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;play 타입에 따라 PerformanceCalculator를 상속받은 서브클래스의 인스턴스를 만들어 준다. Main 클래스는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        main.plays = Main.getPlays();
        main.invoice = Main.getInvoice();
        String result = main.statement(Main.getInvoice(), Main.getPlays());
        System.out.println(result);
    }

    private Map&amp;lt;String, Play&amp;gt; plays;
    private Invoice invoice;
    private PerformanceCalculator performanceCalculator;

    public  String statement(Invoice invoice, Map&amp;lt;String, Play&amp;gt; plays) {
        double totalAmount = 0.d;

        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \\n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            performanceCalculator = createPerformanceCalculator(performance, playFor(performance));
            //함수 인라인
            result.append(playFor(performance).getName()).append(&quot;: &quot;).append(usd(performanceCalculator.amount(performance))).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \\n&quot;);
        }

        result.append(&quot;총액:&quot;).append(usd(totalAmount())).append('\\n');
        result.append(&quot;적립 포인트:&quot;).append(totalVolumeCredits()).append(&quot;점 \\n&quot;);

        return result.toString();
    }

    private double totalAmount() {
        double totalAmount = 0;
        for(Performance performance : invoice.getPerformances()) {
            totalAmount += performanceCalculator.amount(performance);
            //함수 인라인
        }
        return totalAmount;
    }

    private double totalVolumeCredits() {
        double volumeCredits = 0.d;
        for(Performance performance : invoice.getPerformances()) {
            volumeCredits += performanceCalculator.volumeCreditsFor(performance);
            //함수 인라인
        }
        return volumeCredits;
    }

    //함수 추출
    private String usd(double aNumber) {
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
        return numberFormat.format(aNumber/100);
    }

    //함수 추출
    private Play playFor(Performance performance) {
        return plays.get(performance.getPlayId());
    }

    private PerformanceCalculator createPerformanceCalculator(Performance performance, Play play) {
        switch (play.getType()) {
            case &quot;tragedy&quot;:
                return new TragedyCalculator(performance, play);
            case &quot;comedy&quot;:
                return new ComedyCalculator(performance, play);
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + play.getType());
        }
    }

    private static Invoice getInvoice() {
        List&amp;lt;Performance&amp;gt; performances = Arrays.asList(
                new Performance(&quot;hamlet&quot;, 55),
                new Performance(&quot;as-like&quot;, 35),
                new Performance(&quot;othello&quot;, 40)
        );

        return new Invoice(&quot;BigCo&quot;, performances);
    }

    private static Map&amp;lt;String, Play&amp;gt; getPlays() {
        Map&amp;lt;String, Play&amp;gt; plays = new HashMap&amp;lt;&amp;gt;();
        plays.put(&quot;hamlet&quot;, new Play(&quot;Hamlet&quot;, &quot;tragedy&quot;));
        plays.put(&quot;as-like&quot;, new Play(&quot;As You Like It&quot;, &quot;comedy&quot;));
        plays.put(&quot;othello&quot;, new Play(&quot;Othello&quot;, &quot;tragedy&quot;));

        return plays;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement()를 Main 메서드에 만들어줬는데 InvoicePrinter라는 클래스를 생성 후 statement()를 옮기겠다. 코드는 다음과 같다.&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;Main 클래스&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        String result = new InvoicePrinter(plays(), invoice())
                .statement();
        System.out.println(result);
    }

    private static Invoice invoice() {
        List&amp;lt;Performance&amp;gt; performances = Arrays.asList(
                new Performance(&quot;hamlet&quot;, 55),
                new Performance(&quot;as-like&quot;, 35),
                new Performance(&quot;othello&quot;, 40)
        );

        return new Invoice(&quot;BigCo&quot;, performances);
    }

    private static Map&amp;lt;String, Play&amp;gt; plays() {
        Map&amp;lt;String, Play&amp;gt; plays = new HashMap&amp;lt;&amp;gt;();
        plays.put(&quot;hamlet&quot;, new Play(&quot;Hamlet&quot;, &quot;tragedy&quot;));
        plays.put(&quot;as-like&quot;, new Play(&quot;As You Like It&quot;, &quot;comedy&quot;));
        plays.put(&quot;othello&quot;, new Play(&quot;Othello&quot;, &quot;tragedy&quot;));

        return plays;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InvoicePrinter 클래스&lt;/p&gt;
&lt;pre id=&quot;code_1744297179826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class InvoicePrinter {
    private Map&amp;lt;String, Play&amp;gt; plays;
    private Invoice invoice;
    private PerformanceCalculator performanceCalculator;

    public InvoicePrinter(Map&amp;lt;String, Play&amp;gt; plays, Invoice invoice) {
        this.plays = plays;
        this.invoice = invoice;
    }

    public  String statement() {
        StringBuilder result = new StringBuilder(&quot;청구 내역(고객명 : &quot; + invoice.getCustomer() + &quot;) \n&quot;);

        for(Performance performance : invoice.getPerformances()) {
            performanceCalculator = createPerformanceCalculator(performance, playFor(performance));
            //함수 인라인
            result.append(playFor(performance).getName()).append(&quot;: &quot;).append(usd(performanceCalculator.amount(performance))).append(&quot; &quot;).append(performance.getAudience()).append(&quot;석 \n&quot;);
        }

        result.append(&quot;총액:&quot;).append(usd(totalAmount())).append('\n');
        result.append(&quot;적립 포인트:&quot;).append(totalVolumeCredits()).append(&quot;점 \n&quot;);

        return result.toString();
    }

    private double totalAmount() {
        double totalAmount = 0;
        for(Performance performance : invoice.getPerformances()) {
            totalAmount += performanceCalculator.amount(performance);
        }
        return totalAmount;
    }

    private double totalVolumeCredits() {
        double volumeCredits = 0.d;
        for(Performance performance : invoice.getPerformances()) {
            volumeCredits += performanceCalculator.volumeCreditsFor(performance);
        }
        return volumeCredits;
    }

    //함수 추출
    private String usd(double aNumber) {
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
        return numberFormat.format(aNumber/100);
    }


    //함수 추출
    private Play playFor(Performance performance) {
        return plays.get(performance.getPlayId());
    }

    private PerformanceCalculator createPerformanceCalculator(Performance performance, Play play) {
        switch (play.getType()) {
            case &quot;tragedy&quot;:
                return new TragedyCalculator(performance, play);
            case &quot;comedy&quot;:
                return new ComedyCalculator(performance, play);
            default:
                throw new RuntimeException(&quot;알 수 없는 장르:&quot; + play.getType());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Refactoring</category>
      <category>리팩터링</category>
      <category>리팩터링2판</category>
      <category>마틴 파울러</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/39</guid>
      <comments>https://youbin2.tistory.com/39#entry39comment</comments>
      <pubDate>Fri, 11 Apr 2025 00:05:28 +0900</pubDate>
    </item>
    <item>
      <title>[객체지향 프로그래밍] 블랙잭 카드 구현</title>
      <link>https://youbin2.tistory.com/38</link>
      <description>&lt;h1 id=&quot;block-1ae7cc75264e807c8713e51eafdd5f56&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;블랙잭 게임 만들기&lt;/h1&gt;
&lt;p id=&quot;block-1ae7cc75264e80c59893fabef94fa8c2&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://okky.kr/questions/358197&quot;&gt;https://okky.kr/questions/358197&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741525060672&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;초보 개발자에게 권장하는 객체지향 모델링 공부 방법 | OKKY Q&amp;amp;A&quot; data-og-description=&quot;원래 다른 글타래의 답글로 적던 내용인데 다른 분들에게도 혹시 도움이 될까 싶어서 별도 글타래로 옮깁니다. 따라서 다소 맥락이 어색한 부분이 있는 점 양해부탁 드립니다. 아마도 '객체지향&quot; data-og-host=&quot;okky.kr&quot; data-og-source-url=&quot;https://okky.kr/questions/358197&quot; data-og-url=&quot;https://okky.kr/questions/358197&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/J9hD1/hyYm2sf0wZ/nziaGtaZ672OZyTcZzPrtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://okky.kr/questions/358197&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://okky.kr/questions/358197&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/J9hD1/hyYm2sf0wZ/nziaGtaZ672OZyTcZzPrtK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;초보 개발자에게 권장하는 객체지향 모델링 공부 방법 | OKKY Q&amp;amp;A&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;원래 다른 글타래의 답글로 적던 내용인데 다른 분들에게도 혹시 도움이 될까 싶어서 별도 글타래로 옮깁니다. 따라서 다소 맥락이 어색한 부분이 있는 점 양해부탁 드립니다. 아마도 '객체지향&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;okky.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p id=&quot;block-1ae7cc75264e802aa8cbff350beb6d39&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 글을 읽고 객체지향 프로그래밍 연습을 위해 블랙잭 게임을 만들어보게 되었습니다. 챗 지피티에게 블랙잭 룰을 물어보았고, 다음과 같이 정리했습니다.&lt;/p&gt;
&lt;div id=&quot;block-1ae7cc75264e80f79bb7cbfe11d6d955&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;Blackjack Game

1️⃣ 게임 기본 요구사항
✅ 카드 관련
- 카드 덱(Deck)은 52장의 카드(4개의 문양 &amp;times; 13개의 값)로 구성된다.
- 카드는 랜덤하게 섞인 상태에서 게임이 시작될 때 사용된다.
- 각 카드는 특정 값(A=1, J/Q/K=10, 숫자=해당 숫자)을 가진다.
✅ 게임 시작
- 게임이 시작되면 딜러와 플레이어는 각각 2장의 카드를 받는다.
- 플레이어의 카드는 공개되며, 딜러의 카드는 1장만 공개되고 나머지 1장은 숨겨진다.
- 만약 처음 받은 2장의 카드 합이 21(블랙잭) 이면 즉시 승패가 결정된다.
    - 플레이어가 블랙잭이면 승리 (단, 딜러도 블랙잭이면 무승부)
    - 딜러가 블랙잭이고 플레이어가 블랙잭이 아니면 패배

2️⃣ 플레이어 행동 요구사항

✅ 카드 추가 요청 (Hit)
- 플레이어는 원하는 만큼 카드를 추가(Hit)할 수 있다.
- 하지만 카드 합이 21을 초과하면(Bust) 즉시 패배한다.
✅ 카드 유지 (Stand)
- 플레이어는 원하는 순간에 카드 받기를 멈출 수 있다(Stand).

3️⃣ 딜러 행동 요구사항
✅ 딜러의 카드 추가 규칙
- 모든 플레이어가 카드 추가(Hit) 또는 멈추기(Stand)를 결정한 후, 딜러의 숨겨진 카드(홀 카드)를 공개한다.
- 딜러는 16 이하의 점수일 경우 반드시 카드 한 장을 추가해야 한다.
- 딜러는 17 이상의 점수일 경우 추가 카드 없이 멈춘다.
- 딜러도 21을 초과하면(Bust) 즉시 패배하며, 플레이어가 남아 있으면 승리한다.

4️⃣ 승패 판정 요구사항
✅ 일반 승패 판정
- 플레이어가 딜러보다 높은 점수이면 승리한다.
- 플레이어와 딜러의 점수가 같으면 무승부(Push) 로 처리된다.
- 플레이어 점수가 21을 초과하면 즉시 패배하며, 딜러의 추가 행동 없이 종료된다.
✅ 블랙잭 승리 보너스
- 플레이어가 블랙잭(2장의 카드로 21)이고, 딜러는 블랙잭이 아닐 경우 배팅금의 1.5배를 받는다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p id=&quot;block-1ae7cc75264e804c9a09e570b2345a2c&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원래 &amp;lsquo;A&amp;rsquo; 카드는 1 또는 11이 되지만 편의를 위해 1로 고정한 채 개발하기로 결정했습니다.&lt;/p&gt;
&lt;h2 id=&quot;block-1ae7cc75264e80cbbfe4c0a32b15cafb&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;모델링&lt;/h2&gt;
&lt;p id=&quot;block-1ae7cc75264e800c85b3cdcb8864a6d5&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발 전 간단하게 모델링을 해보았습니다. 다음과 같습니다.&lt;/p&gt;
&lt;div id=&quot;block-1ae7cc75264e803a8ed5e9377359dd20&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HEMNV/btsME0skzox/aGFUeBM0DlzMdSypRIasFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HEMNV/btsME0skzox/aGFUeBM0DlzMdSypRIasFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HEMNV/btsME0skzox/aGFUeBM0DlzMdSypRIasFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHEMNV%2FbtsME0skzox%2FaGFUeBM0DlzMdSypRIasFk%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;461&quot; height=&quot;181&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p id=&quot;block-1ae7cc75264e80a5ae40fe23df5aa1a5&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Game 객체는 블랙잭 게임의 순서를 진행하는 역할을 맡았습니다. Dealer와 Player는 각각 딜러와 플레이어를 추상화했습니다. 두 객체 모두 딜러와 플레이어가 카드를 다루는 일을 하는 책임을 할당받았습니다. 카드 덱은 카드를 모아두고 요청에 따라 카드를 섞고 카드를 건네주는 역할을 하고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;block-1ae7cc75264e800cbed2fcfac9239152&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;코드로 살펴보기&lt;/h2&gt;
&lt;h3 id=&quot;block-1b17cc75264e80048483d6d8bf115945&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Game 객체&lt;/h3&gt;
&lt;p id=&quot;block-1ae7cc75264e809d8cbec5e75dd9d0ae&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Game 클래스는 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;div id=&quot;block-1b17cc75264e80ef8fddf3b963abcdf9&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot;&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class Game {
    private Player player;
    private Dealer dealer;
    private Scanner sc = new Scanner(System.in);

    public Game(Player player, Dealer dealer) {
        this.player = player;
        this.dealer = dealer;
    }

    public void run() {
        System.out.println('\n' + '\n' + &quot;Game start&quot;);

        selectTwoCards();
        if(player.getScore() == 21) {
            System.out.println(&quot;player win!!!&quot;);
            return;
        }
        hitAndStand();
        getWinner();
    }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p id=&quot;block-1ae7cc75264e8016b877e8e89f2d3c11&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;run() 메서드에서 게임이 실행됩니다. 앞서 정해둔 룰에 따라 selectTwoCards() 메서드에서 딜러와 플레이어 각각 카드 2장을 뽑고 플레이어의 점수가 21점이면 플레이어가 이긴 채로 그대로 게임이 종료됩니다. 플레이어 점수가 21점 미만이라면 카드 추가 요청(hit)과 카드 유지(stand)하는 과정을 나타내는 hitAndStand() 메서드가 실행됩니다. 이후 누가 이겼는지 결정하는 getWinner() 메서드를 실행합니다. 다음은 전체 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class Game {
    private Player player;
    private Dealer dealer;
    private Scanner sc = new Scanner(System.in);

    public Game(Player player, Dealer dealer) {
        this.player = player;
        this.dealer = dealer;
    }

    public void run() {
        System.out.println('\n' + '\n' + &quot;Game start&quot;);

        selectTwoCards();
        if(player.getScore() == 21) {
            System.out.println(&quot;player win!!!&quot;);
            return;
        }
        hitAndStand();
        getWinner();
    }

    private void getWinner() {
        if (player.getScore() &amp;gt; 21) {
            System.out.println(&quot;player lose!!!&quot;); // 플레이어 점수가 21 초과 &amp;rarr; 무조건 패배
        } else if (dealer.getScore() &amp;gt; 21 || player.getScore() &amp;gt; dealer.getScore()) {
            System.out.println(&quot;player win!!!&quot;); // 딜러 점수가 21 초과하거나, 플레이어 점수가 더 크면 승리
        } else if (player.getScore() == dealer.getScore()) {
            System.out.println(&quot;Push&quot;);
        } else {
            System.out.println(&quot;player lose!!!&quot;); // 나머지 경우는 플레이어 패배
        }
    }

    private void hitAndStand() {
        while (player.getScore() &amp;lt;= 21) {
            System.out.println(&quot;hit or stand&quot;);
            String in = sc.next();
            if(in.equals(&quot;hit&quot;)) {
                player.hit();
                System.out.println(&quot;----- cards of player -----&quot;);
                showCards(player.showCards());
                System.out.println(player.getScore());
            } else if(in.equals(&quot;stand&quot;)) {
                break;
            }
        }

        if(player.getScore() &amp;gt; 21) {
            return;
        }

        System.out.println(&quot;----- cards of dealer -----&quot;);
        showCards(dealer.showCards());
        dealer.hit();
    }

    private void showCards(List&amp;lt;Card&amp;gt; cards) {
        cards.forEach(System.out::println);
        System.out.println(&quot;score : &quot; + cards.stream().mapToInt(Card::getScore).sum());
    }

    private void selectTwoCards() {
        System.out.println(&quot;----- select two cards -----&quot;);
        player.selectTwoCards();
        dealer.selectTwoCards();

        System.out.println(&quot;----- card of player -----&quot;);
        showCards(player.showCards());
        System.out.println(&quot;----- card of dealer -----&quot;);
        System.out.println(dealer.showCard());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;block-1b17cc75264e80ed8fdcc7cbeceb479b&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;딜러 객체&lt;/h3&gt;
&lt;p id=&quot;block-1b17cc75264e80f9a6bef9e42c0acdd0&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;딜러가 게임 클래스부터 받아야 하는 메시지를 정리해보았습니다. 카드 1장만 공개하기, 모든 카드 공개하기, 점수가 16 이상 나올 때까지 카드 뽑기, 점수 계산하기, 카드 2장 뽑기가 나왔습니다. 이를 반영하여 다음과 같이 인터페이스를 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public interface Dealer {
    Card showCard();
    List&amp;lt;Card&amp;gt; showCards();
    void hitWhileBelowSeventeen();
    int getScore();
    void selectTwoCards();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e8004a3ade0760b9291bb&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구현 클래스는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class DealerImpl implements Dealer {
    private CardDeck cardDeck;
    private List&amp;lt;Card&amp;gt; cardList;

    public DealerImpl(CardDeck cardDeck) {
        this.cardDeck = cardDeck;
        cardList = new ArrayList&amp;lt;&amp;gt;();
    }

    @Override
    public Card showCard() {
        return cardList.get(0);
    }

    @Override
    public List&amp;lt;Card&amp;gt; showCards() {
        return cardList;
    }

    @Override
    public void hitWhileBelowSeventeen() {
        System.out.println(&quot;--- dealer hit ---&quot;);
        while(getScore() &amp;lt; 17) {
            Card card = cardDeck.selectCard();
            cardList.add(card);
            System.out.println(&quot;card is &quot; + card);
            System.out.println(&quot;current sum : &quot; + getScore());
        }
    }

    @Override
    public int getScore() {
        return cardList.stream().mapToInt(Card::getScore).sum()
    }

    @Override
    public void selectTwoCards() {
        cardList.add(cardDeck.selectCard());
        cardList.add(cardDeck.selectCard());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;block-1b17cc75264e80a88719cafbdabc60d5&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;딜러가 카드와 카드덱을 참조하고 있는데 어떻게 구현하고 클래스간에 상호작용하는지 뒤에서 알아보도록 하겠습니다. 다음은 플레이어 객체를 어떻게 구현했는지 알아보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;block-1b17cc75264e808bacc7f6e24674e848&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;플레이어 객체&lt;/h3&gt;
&lt;p id=&quot;block-1b17cc75264e80c087ace41d6ef7a52b&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;플레이어가 Game 객체로부터 받을 메시지는 다음과 같습니다. 카드를 모두 공개하라, 카드를 뽑아라(hit) 점수를 계산해라, 카드 2개를 뽑아라. 이 메시지를 반영하여 다음과 같이 인터페이스를 만들었습니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public interface Player {
    List&amp;lt;Card&amp;gt; showCards();
    void hit();
    int getScore();
    void selectTwoCards();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e80c3b0e7c8de23981685&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구현 클래스는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class PlayerImpl implements Player {
    private CardDeck cardDeck;
    private List&amp;lt;Card&amp;gt; cardList;

    public PlayerImpl(CardDeck cardDeck) {
        this.cardDeck = cardDeck;
        cardList = new ArrayList&amp;lt;&amp;gt;();
    }


    @Override
    public List&amp;lt;Card&amp;gt; showCards() {
        return cardList;
    }

    @Override
    public void hit() {
        return cardList.stream().mapToInt(Card::getScore).sum();
    }

    @Override
    public int getScore() {
        int sum = 0;
        for(int i=0;i&amp;lt;cardList.size();++i) {
            sum += cardList.get(i).getScore();
        }
        return sum;
    }

    @Override
    public void selectTwoCards() {
        hit();
        hit();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;block-1b17cc75264e80e3b227ec803bec3cf8&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;카드 객체&lt;/h3&gt;
&lt;p id=&quot;block-1b17cc75264e80d68290cc88480f7299&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카드를 모양, 숫자, 점수를 프로퍼티로 갖고 있도록 추상화하였습니다. 구현한 내용은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class Card {
    public Card(String shape, String number, Integer score) {
        this.shape = shape;
        this.number = number;
        this.score = score;
    }

    private String shape;
    private String number;
    private Integer score;
    public Integer getScore() {
        return score;
    }

    @Override
    public String toString() {
        return &quot;Card{&quot; +
                &quot;shape='&quot; + shape + '\'' +
                &quot;, number='&quot; + number + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;block-1b17cc75264e8092a028c19275f3332e&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;카드덱 객체&lt;/h3&gt;
&lt;p id=&quot;block-1b17cc75264e80d895e0f3b1c7e02da4&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카드덱 클래스가 플레이어와 딜러로부터 받을 메시지는 &amp;lsquo;카드를 뽑아서 전송하라&amp;rsquo;입니다. 이에 맞춰 selectCard 메서드를 만들었습니다. 플레이어와 딜러가 같은 카드덱에 같은 인스턴스를 갖고 있도록 설계했습니다. 멀티 스레드 환경이었다면 동시성 문제가 발생했을 수 있겠지만 그렇지 않았기 때문에 동시성 처리는 따로 하지 않았습니다. 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;public class CardDeck {
    private Map&amp;lt;Integer, Card&amp;gt; cards;
    private Integer cardCount;
    Random random = new Random();

    public CardDeck() {
        this.cards = new HashMap&amp;lt;&amp;gt;();
        this.cardCount = 52;
        initializeDeck();
    }

    private void initializeDeck() {
        String[] shapes = {&quot;&amp;spades;&quot;, &quot;&amp;hearts;&quot;, &quot;&amp;diams;&quot;, &quot;&amp;clubs;&quot;}; // 스페이드, 하트, 다이아몬드, 클럽
        String[] numbers = {&quot;A&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;10&quot;, &quot;J&quot;, &quot;Q&quot;, &quot;K&quot;};

        int index = 0; // 카드 고유 번호 (1~52)

        for (String shape : shapes) {
            for (String number : numbers) {
                int score = getScore(number);
                cards.put(index++, new Card(shape, number, score));
            }
        }
    }

    private int getScore(String number) {
        if (number.equals(&quot;A&quot;)) return 1; // A는 기본 11점
        if (number.equals(&quot;J&quot;) || number.equals(&quot;Q&quot;) || number.equals(&quot;K&quot;)) return 10; // J, Q, K는 10점
        return Integer.parseInt(number); // 숫자 카드 그대로 점수 사용
    }


    public Card selectCard() {
        int randomNumber = random.nextInt(cardCount);
        Card card = cards.get(randomNumber);
        cards.remove(randomNumber);
        cardCount--;
        return card;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e80d09a74c9bf61afad53&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주목해야 할 점은 카드를 초기화하여 덱을 만드는 과정입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;text-align: left;&quot;&gt;&lt;code&gt;private void initializeDeck() {
        String[] shapes = {&quot;&amp;spades;&quot;, &quot;&amp;hearts;&quot;, &quot;&amp;diams;&quot;, &quot;&amp;clubs;&quot;}; // 스페이드, 하트, 다이아몬드, 클럽
        String[] numbers = {&quot;A&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;, &quot;9&quot;, &quot;10&quot;, &quot;J&quot;, &quot;Q&quot;, &quot;K&quot;};

        int index = 0; // 카드 고유 번호 (1~52)

        for (String shape : shapes) {
            for (String number : numbers) {
                int score = getScore(number);
                cards.put(index++, new Card(shape, number, score));
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;block-1b17cc75264e8087810fee16830721bc&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카드에 있는 모양(shapes)과 점수(numbers)를 배열로 만들어 미리 저장했습니다. 4가지 카드 모양(&amp;spades;, &amp;hearts;, &amp;diams;, &amp;clubs;)과 13가지 카드 숫자(A, 2~10, J, Q, K)가 배열에 있어 총 52장의 숫자카드가 만들어질 예정입니다. 이후 2중 for문을 돌며 카드를 생성합니다. 여기서 인덱스는 카드 고유 번호로서 map 자료구조에서 key 값으로 활용됩니다.&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e80fba526fae1401cf53c&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;block-1b17cc75264e80f5a47bee05c02dc120&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;느낀 점&lt;/h2&gt;
&lt;p id=&quot;block-1b17cc75264e804980d2e9f9bcb06d08&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;간단한 게임 룰이지만 요구사항을 모델링하고 코드를 구현하는 과정이 결코 쉽지 않았습니다. 특히 객체 간 협력을 위해 각 객체에 적절한 책임을 부여하는 작업이 어렵게 느껴졌다. &amp;ldquo;이 역할을 이 객체가 맡는 것이 맞을까?&amp;rdquo;라는 질문이 지속적으로 떠올랐으며, 정답이 명확하지 않아 더욱 난관을 겪었습니다.&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e80c182e8d3f168c2b64f&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메서드를 설계할 때에는 &lt;b&gt;하나의 메서드가 하나의 책임만&lt;/b&gt; 갖도록 노력했으며, 상위 메서드에서 도메인 로직을 다루고 하위 메서드에서 구체적인 구현을 맡기려 시도했습니다. 그러나 아직 완벽하게 만족스러운 결과를 얻지는 못했습니다 다. 좋은 코드 작성을 위해 아직 많은 훈련과 반복 학습이 필요하다는 사실을 깨달았습니다.&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e80eaaa87fa3942a63d56&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 코드를 작성하면서 &lt;b&gt;람다식 활용 능력&lt;/b&gt;이 충분히 숙달되지 않았음을 체감했습니다. 람다식을 더욱 편리하게 사용할 수 있도록 학습과 실습이 필요하다고 느꼈습니다.&lt;/p&gt;
&lt;p id=&quot;block-1b17cc75264e8029b561f271cc26df26&quot; style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, 함수&amp;middot;클래스&amp;middot;변수 이름을 짓는 과정에서 &lt;b&gt;도메인&lt;/b&gt;을 충분히 반영하려 노력했으나 미흡한 부분이 있었고, 이 부분도 계속 다듬어 나가야 할 과제임을 알게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/HongYouBin/oop-blackjack-game&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/HongYouBin/oop-blackjack-game&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741525580925&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - HongYouBin/oop-blackjack-game&quot; data-og-description=&quot;Contribute to HongYouBin/oop-blackjack-game development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/HongYouBin/oop-blackjack-game&quot; data-og-url=&quot;https://github.com/HongYouBin/oop-blackjack-game&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbQVal/hyYrQcf5eD/cbvFHQo7wpOy64hnKyhKn1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_140_1068_252,https://scrap.kakaocdn.net/dn/fyWo2/hyYrUyXndx/2Mz4BwLMcjjIgbaJKvVPd0/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_140_1068_252&quot;&gt;&lt;a href=&quot;https://github.com/HongYouBin/oop-blackjack-game&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/HongYouBin/oop-blackjack-game&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbQVal/hyYrQcf5eD/cbvFHQo7wpOy64hnKyhKn1/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_140_1068_252,https://scrap.kakaocdn.net/dn/fyWo2/hyYrUyXndx/2Mz4BwLMcjjIgbaJKvVPd0/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_140_1068_252');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - HongYouBin/oop-blackjack-game&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to HongYouBin/oop-blackjack-game development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>객체지향</category>
      <category>객체지향 프로그래밍</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/38</guid>
      <comments>https://youbin2.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 9 Mar 2025 22:17:05 +0900</pubDate>
    </item>
    <item>
      <title>소프트웨어 테스트 원칙</title>
      <link>https://youbin2.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 1.62em; letter-spacing: -1px;&quot;&gt;소프트웨어 테스트 원칙&lt;/span&gt;&lt;/p&gt;
&lt;article id=&quot;11f7cc75-264e-80cf-a993-fff7131bee9f&quot; class=&quot;page sans&quot;&gt;
&lt;div class=&quot;page-body&quot;&gt;
&lt;h3 id=&quot;11f7cc75-264e-80e8-ac02-d556bb629a28&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;단위 테스트&lt;/h3&gt;
&lt;mark class=&quot;highlight-default_background&quot;&gt;&lt;b&gt;단위 테스트 &lt;/b&gt;&lt;/mark&gt;&lt;mark class=&quot;highlight-default_background&quot;&gt;: 코드의 개별 구성 요소(주로 메서드나 함수)가 올바르게 동작하는지 검증하는 테스트&lt;/mark&gt;
&lt;p id=&quot;1297cc75-264e-8096-9fee-eaf71a058473&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-80c9-ae07-db08bd33d494&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단위 테스트 특집&lt;/b&gt;&lt;/p&gt;
&lt;ol id=&quot;1297cc75-264e-807b-ab3d-ea03815e0cd7&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;독립적&lt;/b&gt;: 단위 테스트는 가능한 한 외부 종속성이나 환경의 영향을 받지 않고 개별 단위의 동작만을 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작은 범위&lt;/b&gt;: 단위 테스트는 함수, 메서드 또는 클래스와 같은 작은 단위를 테스트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동화 가능&lt;/b&gt;: &lt;b&gt;JUnit&lt;/b&gt;같은 테스트 프레임워크를 사용하여 자동화&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;1277cc75-264e-8085-92bc-c3407a1446d5&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

public class CalculatorTest {
    @Test
    void testAdd() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, resul);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;11f7cc75-264e-80dc-bd45-ff9c64627444&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;11f7cc75-264e-809c-93cc-dbc40a94d89d&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;단위 테스트 장점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol id=&quot;11f7cc75-264e-80a8-8ca4-ca29d05dba14&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;높은 테스트 커버리지 :&lt;/b&gt; 단위 테스트는 기능 테스트로 수행하기 어렵거나 불가능한 오류 조건에 대해 쉽게 테스트할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;팀 생산성 향상 : &lt;/b&gt;대규모 어플리케이션 작업에서 단위 테스트를 사용하면 다른 컴포넌트가 준비되지 않아도 테스트 가능하다. &amp;rarr; mock&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;디버깅 작업 감소 : &lt;/b&gt;단위 테스트를 통해 코드 중 어디에 문제가 있는지 구체적인 부분을 알 수 있다. 애플리케이션을 일일이 디버깅할 필요가 줄어든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;리팩터링 용이 : &lt;/b&gt;리팩토링하는 과정에서&amp;nbsp;&lt;b&gt;가장 중요한 것은 &amp;ldquo;결과의 변경 없이&amp;rdquo; 코드의 구조를 변경하는 것이다. &lt;/b&gt;소스를 리팩터링하거나 고칠 때 오류를 만들 위험이 있다. 단위 테스트를 통해 리팩터링에 확신을 갖게 된다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;기능 구현에 도움 :&lt;/b&gt; 단위 테스트는 코드가 독립적으로 테스트될 수 있도록 구조적 설계를 강요한다. 단위 테스트를 고려하지 않으면 테스트하기 어렵고 유지 보수하기 힘든 코드가 만들어진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;코드 문서화 :&lt;/b&gt; 단위 테스트 그 자체로 API 사용 예제가 만들어진다. 테스트 코드를 작성하기만 해도 유스 케이스를 명확히 알 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;다양한 지표 제공 :&lt;/b&gt; 커버리지 지표 및 테스트 통과, 실패 진행사항을 추적할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;11f7cc75-264e-80fc-8e05-f633008c679b&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;통합 테스트&lt;/h3&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;통합 테스트&lt;/b&gt; : 대상 환경에서 실행 가능한 &lt;span style=&quot;background-color: #fbf3db; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;컴포넌트 간의 상호작용을 테스트&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-8098-898c-c55384336b25&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-8050-81ef-e553c6014873&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;시스템의 상호작용을 통해, 단위 테스트에서는 발견하기 어려운 문제를 찾아낼 수 있다.&lt;/p&gt;
&lt;ul id=&quot;1277cc75-264e-8074-b278-dbcf83989abc&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;여러 컴포넌트가 함께 동작하는지를 확인&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;데이터베이스, 파일 시스템 등 외부 리소스와의 상호작용을 포함&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;주로 모의 객체를 사용하지 않고 실제 의존성을 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;11f7cc75-264e-8001-a7cb-d210d984d822&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table id=&quot;11f7cc75-264e-8095-975a-ee3f8cc45a34&quot; class=&quot;simple-table&quot; style=&quot;height: 115px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 151px;&quot;&gt;상호작용&lt;/td&gt;
&lt;td style=&quot;width: 600px;&quot;&gt;테스트 설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;ee653419-cea8-4d11-a784-ac2fa9e35bc8&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;r{][&quot; class=&quot;&quot; style=&quot;width: 151px; height: 38px;&quot;&gt;객체&lt;/td&gt;
&lt;td id=&quot;^PHQ&quot; class=&quot;&quot; style=&quot;width: 600px; height: 38px;&quot;&gt;객체를 인스턴스화 하고 다른 객체의 메서드를 호출한다. &lt;b&gt;다른 클래스에 속한 객체 간에 어떻게 협력해서 문제를 해결할 수 있는지 확인&lt;/b&gt; 가능하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;7b4abb4a-a8ed-431c-a6fb-64220035b189&quot; style=&quot;height: 19px;&quot;&gt;
&lt;td id=&quot;r{][&quot; class=&quot;&quot; style=&quot;width: 151px; height: 19px;&quot;&gt;서비스&lt;/td&gt;
&lt;td id=&quot;^PHQ&quot; class=&quot;&quot; style=&quot;width: 600px; height: 19px;&quot;&gt;애플리케이션을 데이터베이스, 다른 외부 리소스와 함께 테스트 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;11f7cc75-264e-80be-936b-f28c7dbfe7df&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;r{][&quot; class=&quot;&quot; style=&quot;width: 151px; height: 38px;&quot;&gt;서브 시스템&lt;/td&gt;
&lt;td id=&quot;^PHQ&quot; class=&quot;&quot; style=&quot;width: 600px; height: 38px;&quot;&gt;인터페이스인 프런트엔드와 비즈니스 로직인 백엔드가 구분된 아키텍쳐를 가진 애플리케이션에서 통합 테스트 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;11f7cc75-264e-8046-975a-d64c0342673e&quot; class=&quot;code arduino&quot;&gt;&lt;code&gt;public class EmailService {
    public boolean sendEmail(String email, String message) {
        // 이메일 전송 로직 (예를 들어 실제 이메일 서버와 통신)
        return true;
    }
}

public class UserService {
    private EmailService emailService;

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public boolean registerUser(String email) {
        // 사용자 등록 로직 후 이메일 전송
        return emailService.sendEmail(email, &quot;Welcome!&quot;);
    }
}

public class UserServiceIntegrationTest {

    @Test
    void testRegisterUser() {
        // 실제 EmailService 사용, Mock 객체 사용하지 않는다
        EmailService emailService = new EmailService();
        UserService userService = new UserService(emailService);
        
        boolean result = userService.registerUser(&quot;test@example.com&quot;);
        assertTrue(result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-800f-ac76-c5219e02dc83&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-80ac-8d79-f8ae780d8ecf&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;시스템 테스트&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80b9-bc2a-c983f7f4ce75&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 테스트&lt;/b&gt; : 시스템이 구체화된 요구 사항을 만족하는지 평가하기 위해 완전한 통합 환경에서 수행하는 테스트 소프트웨어가 실제 환경에서 제대로 작동하는지 확인하는 마지막 단계 중 하나이다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;11f7cc75-264e-8031-a304-cc15dfcd7c9c&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 테스트 목적&lt;/b&gt;&lt;/p&gt;
&lt;ul id=&quot;1267cc75-264e-8039-a90b-ea93caa805fe&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&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;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;모든 모듈과 기능이 &lt;b&gt;의도한 대로 작동하는지&lt;/b&gt; 확인.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;성능, 보안, 신뢰성&lt;/b&gt; 등의 &lt;b&gt;비기능적 요구사항&lt;/b&gt;도 만족하는지 확인.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;의존성&lt;/b&gt;: 실제 데이터베이스, 네트워크, API, 파일 시스템 등 외부 리소스를 포함한 전체 환경을 설정하여 테스트합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;1267cc75-264e-8063-8b99-e60abbb81030&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;시스템 테스트 환경에서 모든 외부 시스템이 항상 사용 가능한 것은 아니기 때문에 테스트 더블이나 모의 객체를 사용한다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 더블&lt;/b&gt; : &lt;/p&gt;
&lt;mark class=&quot;highlight-default_background&quot; style=&quot;font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;테스트 대상 객체를 대신하는 모든 대체 객체. 실제로 동작할 필요는 없고, 테스트 시에 &lt;/mark&gt;&lt;mark class=&quot;highlight-default_background&quot; style=&quot;font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;&lt;b&gt;필요한 상황을 시뮬레이션하거나 제어하기 위해 사용&lt;/b&gt;&lt;/mark&gt;&lt;mark class=&quot;highlight-default_background&quot; style=&quot;font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;된다. 테스트 더블은 실제 의존성 대신 사용되므로, 테스트 환경에서 복잡한 설정이나 외부 의존성을 최소화할 수 있고, 특정 조건을 쉽게 테스&lt;/mark&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;트할 수 있다.&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-809b-8516-d883834afef1&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-80f8-b9ff-c962eb797731&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스텁&lt;/b&gt;&lt;/p&gt;
&lt;ul id=&quot;1297cc75-264e-80c0-9c53-da1237cc97b8&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&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;b&gt;미리 정의된 행동&lt;/b&gt;을 제공&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;특정 입력&lt;/b&gt;에 대해 &lt;b&gt;예상되는 출력을 반환&lt;/b&gt;하는 용도로 사용&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;외부 컴포넌트 동작을 단순화하여 미리 정해진 응답 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;1287cc75-264e-800d-8b0e-d242c5a863e1&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모킹 &lt;/b&gt;&lt;/p&gt;
&lt;ul id=&quot;1297cc75-264e-800a-9200-dcdb8ef895b3&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&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;b&gt;호출 횟수&lt;/b&gt;, &lt;b&gt;호출된 인자&lt;/b&gt; 등을 &lt;b&gt;검증&lt;/b&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;단순히 값을 반환하는 것에 그치지 않고 &lt;b&gt;행동을 관찰&lt;/b&gt;하고, &lt;b&gt;검증&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;1297cc75-264e-8076-835a-e38b64c30fb2&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-8097-9b56-f9c97cda40d7&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;인수 테스트&lt;/h3&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인&lt;/b&gt;&lt;span style=&quot;color: #37352f; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;&lt;b&gt;수 테스트&lt;/b&gt;는 애플리케이션이 사용자의 요구사항을 충족하는지, 즉 비즈니스 요구사항과 기능적 요구사항을 만족하는지 검증하는 테스트 과정. &lt;/span&gt;인수 테스트는 소프트웨어가 실제로 출시되기 전에 최종 사용자가 기대한 대로 동작하는지 확인하는 마지막 단계로 볼 수 있다. &amp;rarr; 사용자 관점&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8033-b442-c593d1644782&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-80d4-82c4-d8bdb69f58bd&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;인수 테스트는 Given, When, Then 키워드를 사용하여 표현하기도 한다. Given, When, Then은 BDD(Behavior Driven Development)에서 사용하는 시나리오 기반 테스트 작성 방식의 핵심 구성 요소이다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Given (주어진 상황) : &lt;/b&gt;테스트의 초기 상태 또는&lt;b&gt; 전제 조건&lt;/b&gt;을 설명&lt;br /&gt;&lt;b&gt;When (행동)&lt;/b&gt; : 사용자가 시스템에서 수행하는 &lt;b&gt;특정 행동이나 이벤트&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8056-9737-c0b61df77dd8&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Then (결과) : 기대되는 결과&lt;/b&gt;를 설명한다. 특정 행동이 발생한 후 시스템이 어떻게 반응해야 하는지를 설명한다. 비즈니스 &lt;b&gt;목표&lt;/b&gt;를 달성했는지 확인 가능.&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-80fe-be6c-e8f76def975a&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-8096-b8d0-f68d94a7f6b1&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[예시]&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-8056-8908-f164c1e1d097&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;기능 : 사용자 주식 트레이드&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-8008-81aa-ff3b27db900e&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;시나리오 : 트레이드가 마감되기 전에 사용자가 판매를 요청&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-80cd-9489-c40602fcac32&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-8057-bcdf-f71f65ff0b01&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;Given&quot; 나는 MSFT 주식을 100 가지고 있다. 그리고 나는 APPL 주식을 150 가지고 있다.&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-800b-a404-ce31d21c4900&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;When&quot; 나는 MSFT 주식 20을 팔도록 요청했다.&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-80ac-966a-fc5b97f14ae0&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;Then&quot; 나는 MSFT 주식 80 가지고 있어야 한다. 그리고 MSFT 주식 20이 판매 요청이 실행되었어야 한다.&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8066-9b49-c5dc9f2d7090&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;1267cc75-264e-8023-9142-ca67b794cebe&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;블랙박스 테스트 VS 화이트박스 테스트&lt;/h2&gt;
&lt;h3 id=&quot;1267cc75-264e-806b-9f7e-c8ee583075d9&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;블랙박스 테스트&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80f4-8df2-f0c3cd8aa9b6&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;블랙 박스 테스트는 시스템의 내부 상태나 동작에 대한 지식이 없을 때 수행하는 테스트다. 주로 &lt;b&gt;입력&lt;/b&gt;과 &lt;b&gt;출력&lt;/b&gt;에만 집중하여 시스템의 작동 여부를 확인한다. 블랙박스 테스트는 외부적인 시스템 인터페이스를 사용한다.&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-80c5-a41b-d5ed4a9ac240&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-8047-b6e1-c690bfd95cdc&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;화이트박스 테스트&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80b6-9d47-f06d0864c068&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;화이트박스 테스트는 소프트웨어의 &lt;b&gt;내부 구조, 코드, 알고리즘&lt;/b&gt;을 기반으로 시스템을 테스트하는 방법. 테스트 작성자는 소프트웨어의 코드를 분석하고, 코드의 흐름이나 내부 동작을 기반으로 테스트 케이스를 설계한다. 주로 &lt;b&gt;개발자&lt;/b&gt;가 코드의 로직과 흐름을 검증하는 데 사용한다.&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8053-b47a-fd5395ef26dc&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-80a5-be8b-deafe1322784&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;장단점 비교&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-8030-879e-fb81a6738d7a&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;table id=&quot;1267cc75-264e-8096-9f54-ff8e227d71a3&quot; class=&quot;simple-table&quot; style=&quot;height: 113px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 345px; height: 17px;&quot;&gt;블랙박스 테스트&lt;/td&gt;
&lt;td style=&quot;width: 345px; height: 17px;&quot;&gt;화이트박스 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;43a7b358-3665-4c65-8919-d529d7faed5d&quot; style=&quot;height: 19px;&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px; height: 19px;&quot;&gt;비개발자여도 테스트 할 수 있다.&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px; height: 19px;&quot;&gt;GUI가 필요하지 않다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;f6feaf25-b30f-4644-bb02-266ca298ed2e&quot; style=&quot;height: 19px;&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px; height: 19px;&quot;&gt;개발과 독립적으로 수행할 수 있다.&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px; height: 19px;&quot;&gt;개발자가 제어하므로 다양한 실행 경로를 커버할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;1287cc75-264e-8073-93fb-c6745d878fd1&quot; style=&quot;height: 38px;&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px; height: 38px;&quot;&gt;사용자 중심적이며, 설계 명세와 다른 부분이 무엇인지 알 수 있다.&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px; height: 38px;&quot;&gt;작은 버그나 예외적인 상황에서 발생할 수 있는 결함을 찾는 데 유리하다. 특히 경계값 분석에 효과적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p id=&quot;1267cc75-264e-80bf-b3b7-ca10dfb54420&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;table id=&quot;1267cc75-264e-80a2-967f-f52c434c427e&quot; class=&quot;simple-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 345px;&quot;&gt;블랙박스 테스트&lt;/td&gt;
&lt;td style=&quot;width: 345px;&quot;&gt;화이트박스 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;2906eb32-2a4a-464f-9beb-966d52946ba2&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;입력할 수 있는 경우의 수 제한적&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;프로그래밍 지식 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;fff20a75-7617-47d7-ac96-0f0d9fdd1fef&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;커버되지 않은 테스트 있을 수 있다.&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;구현 변경시 테스트 다시 작성해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;1287cc75-264e-8097-970d-eae17bf038fd&quot;&gt;
&lt;td id=&quot;QCoq&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;테스트 중복 가능.&lt;/td&gt;
&lt;td id=&quot;G_Dk&quot; class=&quot;&quot; style=&quot;width: 345px;&quot;&gt;테스트와 구현 결합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p id=&quot;1277cc75-264e-80bf-847c-d30d82054cf7&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;빠른 사용자 피드백이 필요하거나 가능 &amp;rarr; 블랙박스 테스트&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80c0-a924-ebf55a0699ed&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;수동으로 실행할 수 있는 테스트 스크립트나 GUI를 제공한다면 고객이 애플리케이션에 대해 직관적으로 알 수 있다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-8078-b5f0-f2fd3b2ae961&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-80db-a3f1-f37a80d3ca97&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 커버리지가 필요한 경우 &amp;rarr; 화이트박스 테스트&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-8038-8529-c3eb78bebe84&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;여러 유형의 테스트를 활용하여 코드 커버리지를 높일 수 있고, 애플리케이션을 리팩터링 하거나 개선하는 데에 유리하다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;1267cc75-264e-80bd-9d69-f58d88c008ff&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;테스트 커버리지&lt;/h2&gt;
&lt;p id=&quot;1267cc75-264e-805c-ac02-f3001cb5ede4&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 커버리지(Test Coverage)는 소프트웨어 테스트 과정에서 코드의 얼마나 많은 부분이 테스트되었는지를 측정하는 지표이다. 가장 기본적인 지표는 다음과 같다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구문 커버리지&lt;/b&gt; : 코드의 각 문장이 실행되었는지 측정한다. 코드 한 줄이 한 번 이상 실행된다면 충족.&lt;/p&gt;
&lt;pre id=&quot;1287cc75-264e-8086-8b8f-fab50f8569e3&quot; class=&quot;code arduino&quot;&gt;&lt;code&gt;public class Calculator {
    public String divide(int a) {
		    System.out.println(&quot;calculate&quot;)//1
        if (a &amp;gt; 0) { //2
            return &quot;Cannot divide by zero&quot;; //3
        }
        return String.valueOf(a); //4
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1287cc75-264e-80ed-9e5a-e6c4669027dd&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;a가 음수일 시 3개의 구문만 실행 &amp;rarr; 커버리지는 75%(3/4)&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-800f-9f90-e72a39f3723f&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-802f-a5a4-f43d8b69ec3a&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브랜치 커버리지&lt;/b&gt; : 조건문(예: if-else)의 각 분기가 테스트되었는지 확인합. 코드 내 모든 분기점에서 가능한 모든 경로가 테스트되었는지 측정&lt;/p&gt;
&lt;pre id=&quot;1287cc75-264e-80eb-95c4-d22927fb70f7&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;public void checkNumber(int num) {
    if (num &amp;lt; 0) { //분기 1
        ...
    } else if(num == 0) { //분기 2
        ...
    } else { //분기 3
			  ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1287cc75-264e-8069-b4eb-c17a6ebbcde4&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-8096-87cf-cfbc400a0308&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;조건 커버리지 : 각 개별 조건이 true, false 값을 가질 수 있도록 테스트하는 것을 목표로 한다.&lt;/p&gt;
&lt;pre id=&quot;1287cc75-264e-805d-9ca3-d5639787769b&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;public boolean isValid(int age, boolean isMember) {
    return age &amp;gt; 18 &amp;amp;&amp;amp; isMember;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul id=&quot;1287cc75-264e-808c-92fd-c4f862ae2947&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;code&gt;age = 20, isMember = true&lt;/code&gt;: 두 조건 모두 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1287cc75-264e-8083-899d-c1562625299f&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;code&gt;age = 15, isMember = true&lt;/code&gt;: 첫 번째 조건 &lt;code&gt;false&lt;/code&gt;, 두 번째 조건 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1287cc75-264e-809b-83ed-e42e9b16183c&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;code&gt;age = 20, isMember = false&lt;/code&gt;: 첫 번째 조건 &lt;code&gt;true&lt;/code&gt;, 두 번째 조건 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1287cc75-264e-80be-a463-c25fdd2d0d38&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;code&gt;age = 15, isMember = false&lt;/code&gt;: 두 조건 모두 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p id=&quot;1287cc75-264e-80c2-9855-d6d805cbd629&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote id=&quot;1287cc75-264e-807a-849d-fbc14ec7c575&quot; class=&quot;&quot; data-ke-style=&quot;style1&quot;&gt;얼마만큼의 코드를 자동화한 단위 테스트로 계산해야 할까? 대답할 필요조차 없다. 모조리 다 해야 한다. 모. 조. 리! 100% 테스트 커버리지를 권장하냐고? 권장이 아니라 강력히 요구한다. 작성한 코드는 한 줄도 빠짐없이 전부 테스트해야 한다. 군말은 필요 없다.&amp;nbsp;― 클린 코더 (로버트 마틴 저)&lt;/blockquote&gt;
&lt;blockquote id=&quot;1287cc75-264e-80c6-bd8d-d1ada8ad7e34&quot; class=&quot;&quot; data-ke-style=&quot;style1&quot;&gt;소프트웨어의 본질은 해당 소프트웨어의 사용자를 위해 도메인에 관련된 문제를 해결하는 능력에 있다. 그 밖의 매우 중요하다 할 수 있는 기능도 모두 이러한 기본적인 목적을 뒷받침하는 데 불과하다. - 에릭 에반스&lt;/blockquote&gt;
&lt;p id=&quot;1287cc75-264e-808a-b6fd-ee112c0f3184&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-803f-81bd-ed905b49269f&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;높은 테스트 커버리지가 필요한 경우&lt;/b&gt;&lt;/p&gt;
&lt;ol id=&quot;1287cc75-264e-80ce-8c5b-dc84106c8658&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 기능&lt;/b&gt;: 핵심적인 비즈니스 로직이나 보안 관련 코드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오픈 소스나 공공 API&lt;/b&gt;: 많은 사용자나 다른 개발자가 사용하는 라이브러리나 API는 높은 테스트 커버리지 필요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 코드&lt;/b&gt;: 복잡한 로직이 많을수록 테스트 커버리지를 높이는 것이 유리 &amp;rarr; 복잡한 부분일수록 예상치 못한 문제들이 발생할 가능성이 높다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p id=&quot;1297cc75-264e-80dd-bf9e-e6222881efb9&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-8057-8716-d4253633fd7d&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 테스트는 중요한 곳에 집중해야 하고 개발자에게 큰 문제가 없다는 확신을 주어야 한다. &lt;/b&gt;복잡성이 높은 곳에 인지 부하로 인해 버그가 생길 가능성이 높고, 비즈니스 로직이 존재하는 도메인 모델 혹은 알고리즘에 문제가 생기면 단순히 시스템 오류로 끝나지 않는다.&lt;/p&gt;
&lt;h2 class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;1267cc75-264e-80ba-a3df-ea09b78dcea5&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;테스트하기 쉬운 코드&lt;/h2&gt;
&lt;h3 id=&quot;1267cc75-264e-804f-9324-f71a29d67361&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;public API 테스트&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80fa-b060-d016fe4381bf&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;퍼블릭 API는 외부 코드나 시스템에서 의존하고 있을 가능성이 크다. API가 변경되거나 잘못 작성되면 해당 API를 사용하고 있는 모든 외부 코드가 제대로 작동하지 않을 수 있다. 변하지 않고 오류 없는 public 메서드를 만든 후 철저한 테스트를 통해 위험을 방지해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-80b9-ac52-c02ae564e9b9&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;의존성 줄이기&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80b1-8257-de42bff31333&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;클래스 내에서 새로운 객체를 직접 또는 간접적으로 인스턴스화하면, 그 클래스는 해당 객체에 강하게 결합되어(타이트 커플링) 의존성이 커진다. 이는 클래스 내부에서 직접 객체를 생성하므로, 테스트 시 해당 객체를 모킹(Mock)하거나 대체하기 어려워진다.&lt;/p&gt;
&lt;pre id=&quot;1277cc75-264e-80d5-963e-d4bc4159327c&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;class Vehicle {
    Driver d;
    boolean hadDriver = true;

    Vehicle(Driver d) {
        this.d = d;
    }

    private void setHadDriver(boolean hasDriver) {
        this.hadDriver = hasDriver;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-8022-bdc2-c198c7b1d2e5&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-802b-8f59-f257235fce11&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;간단한 생성자 만들기&lt;/h3&gt;
&lt;p id=&quot;1277cc75-264e-8016-9dda-ee7696ea4bd3&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 단계&lt;/p&gt;
&lt;ol id=&quot;1277cc75-264e-80ba-8f84-f0449b991a2b&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;테스트할 클래스 인스턴스화&lt;/li&gt;
&lt;li&gt;클래스를 특정 상태로 설정&lt;/li&gt;
&lt;li&gt;작업을 수행&lt;/li&gt;
&lt;li&gt;클래스 상태 검증&lt;/li&gt;
&lt;/ol&gt;
&lt;p id=&quot;1287cc75-264e-803b-b5bf-d5536f940090&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;생성자가 없다면 1번, 2번을 같이 수행하게 된다. 테스트 케이스에서 생성자에 클래스를 미리 정의하면 다양한 상태를 만들기 힘들다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80a2-b790-e369712dc4b5&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-802c-b851-fe769bd8be00&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 클래스를 특정 상태로 설정하는 것은 별도의 작업으로 분리해야 한다. 테스트 케이스에서 동일한 객체를 여러 시나리오로 재사용할 때, setter를 이용해 객체의 일부 상태만 변경하면서 다양한 경우를 테스트할 수 있다. 이 방식은 객체를 매번 새로 생성하지 않으면서 테스트 코드의 중복을 줄이는 데 도움을 준다.&lt;/p&gt;
&lt;pre id=&quot;1277cc75-264e-8058-985e-fd1367a7980b&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;class Car {
    private int maxSpeed;
    
    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-8080-b3b9-cf00d631d51c&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-802f-854e-d5115bd31bde&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;데메테르 법칙 따르기&lt;/h3&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데메테르 법칙&lt;/b&gt;(Law of Demeter)은 객체 지향 설계에서 &lt;span style=&quot;background-color: #fbf3db; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;&quot;잘 알지 못하는 객체와 상호작용하지 말라&quot;는 원칙&lt;/span&gt;을 의미한다. 객체는 자신이 직접적으로 알고 있는 객체(예: 자신의 필드, 메서드 파라미터, 메서드에서 생성된 객체)와만 상호작용해야 하며, 메서드 체이닝이나 연속적인 접근을 통해 다른 객체의 내부 객체를 직접 접근하는 것을 피해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-8040-8fac-dcba1d9e944a&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;데메테르 법칙의 구체적인 규칙&lt;/h3&gt;
&lt;ul id=&quot;1267cc75-264e-80db-9abe-f23e1c0f0a4b&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&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;(this)과만 상호작용해야 한다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;객체는 &lt;b&gt;자신의 필드&lt;/b&gt;(인스턴스 변수)와 상호작용할 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;객체는 &lt;b&gt;자신의 메서드에서 생성된 객체&lt;/b&gt;와 상호작용할 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;객체는 &lt;b&gt;메서드의 매개변수&lt;/b&gt;로 전달된 객체와 상호작용할 수 있다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;객체는 &lt;b&gt;이러한 객체들의 직접적인 메서드&lt;/b&gt;&lt;span style=&quot;font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Display', 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; letter-spacing: 0px;&quot;&gt;만 호출할 수 있다. 즉, 객체는 &quot;친구의 친구&quot;에게 메시지를 전달하지 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;1267cc75-264e-8015-8c0d-cf3bf5e20a31&quot; class=&quot;code arduino&quot;&gt;&lt;code&gt;public class Order {
    private Customer customer;

    public Customer getCustomer() {
        return customer;
    }
}

public class Customer {
    private Address address;

    public Address getAddress() {
        return address;
    }
}

public class Address {
    private String city;

    public String getCity() {
        return city;
    }
}

// 데메테르 법칙 위반 코드
public class OrderService {
    public void printCustomerCity(Order order) {
        // Order -&amp;gt; Customer -&amp;gt; Address -&amp;gt; city에 접근
        System.out.println(order.getCustomer().getAddress().getCity());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-8069-91e3-f3eb70d07a68&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8009-b88b-dfeb1e08c802&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;데메테르 법칙 위반 예시 테스트. Address, Customer, Order를 모두 모킹해야 하므로 좋지 않은 테스트이다.&lt;/p&gt;
&lt;pre id=&quot;1267cc75-264e-804c-979f-d3664e2a3ede&quot; class=&quot;code reasonml&quot;&gt;&lt;code&gt;public class OrderServiceTest {

    @Test
    public void testPrintCustomerCity() {
        // 여러 단계의 객체 모킹 필요
        Address mockAddress = mock(Address.class);
        when(mockAddress.getCity()).thenReturn(&quot;New York&quot;);

        Customer mockCustomer = mock(Customer.class);
        when(mockCustomer.getAddress()).thenReturn(mockAddress);

        Order mockOrder = mock(Order.class);
        when(mockOrder.getCustomer()).thenReturn(mockCustomer);

        OrderService orderService = new OrderService();
        orderService.printCustomerCity(mockOrder);

        verify(mockAddress).getCity();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-80a7-b9fb-d8b4379c24b8&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1267cc75-264e-80f6-b299-d31a80f3f1e8&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;숨은 의존성과 전역 상태 피하기&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80b0-b16f-eb3302969998&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;숨은 의존성&lt;/b&gt; : 코드에서 명시적으로 드러나지 않지만 내부적으로 다른 객체나 모듈에 의존하는 경우를 뜻한다. 숨은 의존성은 객체가 어떤 다른 객체나 리소스에 의존하고 있는지를 알기 어렵게 만든다. &amp;rarr; 모킹 어려워진다.&lt;/p&gt;
&lt;p class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-80f7-bb89-f53095659d81&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;OrderRepository를 모킹하기 어려워진다.&lt;/p&gt;
&lt;pre id=&quot;1267cc75-264e-8092-9e85-c545060f4f13&quot; class=&quot;code cpp&quot;&gt;&lt;code&gt;public class OrderService {
    // 숨은 의존성: OrderRepository를 직접 생성
    private OrderRepository orderRepository = new OrderRepository();

    public Order findOrderById(int id) {
        return orderRepository.findById(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1267cc75-264e-80f2-9d62-f8022da2660b&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-809d-bef7-e9d7ba64c088&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전역 상태&lt;/b&gt; : 애플리케이션에서 여러 곳에서 공유되고 접근 가능한 상태(예: 전역 변수, 싱글톤 객체, 스태틱 필드 등)를 의미&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8070-990a-e75978015070&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;전역 객체에만 접근을 공유하는 게 아니라 전역 객체가 참조하는 모든 객체를 공유하는 문제점 발생한다.&lt;/p&gt;
&lt;pre id=&quot;1267cc75-264e-8075-a89d-e6316e0cc34a&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;class Reservation {
    public void makeReservation() {
        Manager manager = Manager.getManager();
        ..
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1267cc75-264e-80ed-9982-ec8f639d8a70&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;제너릭 메서드 사용하기&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-80b7-9097-e06bb8442863&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;정적 코드(static)을 사용 시 다형성을 활용하지 못하면 애플리케이션과 테스트에 코드를 재사용하지 않게 된다. 이런 상황은 애플리케이션과 테스트에서 코드 중복이 생길 수 있어 피하는 게 좋다.&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-80bc-83a8-d3ffaec367fc&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1287cc75-264e-806b-8f78-da1c0f7cd14b&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;제네릭 메서드를 사용하면 &lt;b&gt;하나의 테스트 메서드로 여러 타입을 동시에 테스트&lt;/b&gt;할 수 있어, 테스트 코드의 양도 줄고 중복 테스트를 피할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;1287cc75-264e-809f-aba2-cff8104b4059&quot; class=&quot;code reasonml&quot;&gt;&lt;code&gt;//제너릭 사용 X -&amp;gt; 코드 중복
public class Utils {
    public static String getFirstString(List&amp;lt;String&amp;gt; list) {
        return list.isEmpty() ? null : list.get(0);
    }

    public static Integer getFirstInteger(List&amp;lt;Integer&amp;gt; list) {
        return list.isEmpty() ? null : list.get(0);
    }
}

//제너릭 사용
public class Utils {
    public static &amp;lt;T&amp;gt; T getFirstElement(List&amp;lt;T&amp;gt; list) {
        return list.isEmpty() ? null : list.get(0);
    }
}

@Test
public void testGetFirstElement() {
    List&amp;lt;String&amp;gt; stringList = Arrays.asList(&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;);
    List&amp;lt;Integer&amp;gt; integerList = Arrays.asList(1, 2, 3);

    // 같은 메서드를 다양한 타입으로 테스트 가능
    assertEquals(&quot;apple&quot;, Utils.getFirstElement(stringList));
    assertEquals(Integer.valueOf(1), Utils.getFirstElement(integerList));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1267cc75-264e-80f0-9222-fe509a5d5fed&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;상속보다는 합성&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-8034-a055-dbbff8a63cb9&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;상속은 클래스 간 강한 결합을 유발한다. 반면 합성(composition)은 객체의 기능을 독립된 구성 요소로 분리하여 주입할 수 있기 때문에, 필요한 부분만을 쉽게 교체하거나 모의(mock)할 수 있어 테스트가 더 유연해진다.&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-805b-8ef1-cadb3df50399&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상속&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;1297cc75-264e-8023-86ff-febaf3d3dce0&quot; class=&quot;code scala&quot;&gt;&lt;code&gt;// Engine 클래스
public class Engine {
    public void start() {
        System.out.println(&quot;엔진이 시동을 겁니다.&quot;);
    }
}

// Car 클래스 (Engine을 상속)
public class Car extends Engine {
    public void drive() {
        System.out.println(&quot;자동차가 주행합니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1297cc75-264e-80bd-a0f5-c8ea83da1969&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1297cc75-264e-803e-abe6-f76b44995c41&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;1277cc75-264e-804d-9b89-e6dc78c5ee9e&quot; class=&quot;code arduino&quot;&gt;&lt;code&gt;// Engine 클래스
public class Engine {
    public void start() {
        System.out.println(&quot;엔진이 시동을 겁니다.&quot;);
    }
}

// Car 클래스 (Engine을 합성)
public class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    public void startCar() {
        engine.start();
        System.out.println(&quot;자동차가 시동을 겁니다.&quot;);
    }

    public void drive() {
        System.out.println(&quot;자동차가 주행합니다.&quot;);
    }
}

// 테스트 코드에서 Mock 엔진 주입. 독립적인 테스트 가능하다.
class CarTest {
    @Test
    void testStart() {
        Engine mockEngine = Mockito.mock(Engine.class); // Mock 객체 생성
        Car car = new Car(mockEngine);
        car.start();
        verify(mockEngine).start(); // 엔진의 start 메서드가 호출되었는지 검증
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1267cc75-264e-8085-912e-d174fe916eef&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;분기문보다 다형성 활용&lt;/h3&gt;
&lt;p id=&quot;1267cc75-264e-806d-8659-cce46f5c4d7b&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;분기문이 많으면 클래스가 복잡해지고 인스턴스화가 어려울 수 있다. 다형성을 사용하면 객체를 인터페이스나 부모 클래스를 통해 주입받기 때문에, 테스트할 때 쉽게 모의 객체로 교체할 수 있다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-800a-a435-cf7cde446a70&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80ba-941b-d0508d47a8a2&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;코드가 다음과 같을 때 테스트 코드를 알아보자.&lt;/p&gt;
&lt;pre id=&quot;1277cc75-264e-801c-9386-e91efd4a62a5&quot; class=&quot;code scala&quot;&gt;&lt;code&gt;public class DocumentPrinter {
    public void printDocument(Document document) {
        document.pringDocument();
    }
}

public abstract class Document {
    public abstract void printDocument();
}

public class WordDocument extends Document {
    @Override
    public void printDocument() {
        printWORDDocument();
    }
}

public class PDFDocument extends Document {
    @Override
    public void printDocument() {
        printPDFDocument();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1277cc75-264e-8002-974b-c4abe823fb31&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-802c-be2b-c4937c2fc1bd&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;1277cc75-264e-80d0-ab39-fcac2f050365&quot; class=&quot;code reasonml&quot;&gt;&lt;code&gt;public class DocumentPrinterMockTest {

    @Test
    void testPrintWordDocumentWithMock() {
        Document wordDocument = Mockito.mock(WordDocument.class); 
        DocumentPrinter printer = new DocumentPrinter();
        printer.printDocument(wordDocument);
      
        verify(wordDocument, times(1)).printDocument();
    }

    @Test
    void testPrintPDFDocumentWithMock() {
        Document pdfDocument = Mockito.mock(PDFDocument.class);
        DocumentPrinter printer = new DocumentPrinter();
        printer.printDocument(pdfDocument);
    
        verify(pdfDocument, times(1)).printDocument();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;1277cc75-264e-8027-8aa9-d82b39e76466&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;1277cc75-264e-80ef-8f60-fccd495e88f8&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;결론 : 좋은 코드는 테스트 하기 쉽다.&lt;/h3&gt;
&lt;blockquote id=&quot;1277cc75-264e-80a8-a3bf-d6068c98d536&quot; class=&quot;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;좋은 코드는 &amp;ldquo;변경하기 쉬운&amp;rdquo;이라는 형용사를 가지고 있는데요.&lt;/b&gt;
&lt;p id=&quot;1277cc75-264e-8051-a340-eac571b6f834&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;이 의미는 약한 결합도를 가지고 있는 코드를 뜻하며 반대로 강결합이 되어있는 코드는 유지비용이 증감되어 저품질코드로 분류됩니다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-808d-ba88-f3cc39ddb986&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;그렇다면 강결합으로 이루어진 코드를 테스트하기 쉬울까요?&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-804b-b500-d3f5a322b698&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;당연히 매우 어렵습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80ef-b211-c1651cc929f4&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부의 영향을 받거나 내부적으로 의존성을 가지고 있는 코드는 변경에 유연하게 대응하지 못하고 재사용하기 어려운 코드들입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-8056-9f94-df5b1eeeee44&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 테스트를 작성하기 어려운 코드가 만들어지는 것입니다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-808c-ab01-d38f741b4374&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다고 테스트하기 쉬운 코드가 모두 좋은 코드가 되는 것은 아닙니다. 하지만 저희는 테스트를 작성하면서 하나의 좋은 코드의 지표를 아래와 같이 세울 수 있게 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-8036-a4f0-d5557cdd17c0&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;만약 내가 작성한 코드가 테스트하기 어려운 코드라면 냄새나는 코드일 가능성이 높아.&amp;rdquo;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80ab-8865-eb59ac31ab1b&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80d6-8d64-e7a3b36672ba&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;자료출처 : &lt;a href=&quot;https://tech.inflab.com/20230404-test-code/#%EB%84%A4%EB%B2%88%EC%A7%B8-%EC%A2%8B%EC%9D%80-%EC%BD%94%EB%93%9C%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0-%EC%89%BD%EB%8B%A4&quot;&gt;https://tech.inflab.com/20230404-test-code/#네번째-좋은-코드는-테스트하기-쉽다&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol id=&quot;1277cc75-264e-8036-8a60-e3b8c4917302&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단일 책임 원칙 : 테스트 작성 단순&lt;/li&gt;
&lt;li&gt;의존성 주입 : mock 객체 쉽게 사용 가능&lt;/li&gt;
&lt;li&gt;의존성 최소화 : 의존성이 적어질수록 테스트 쉬워진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;1267cc75-264e-80a8-b5f3-e35e63f696ee&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;TDD&lt;/h2&gt;
&lt;p id=&quot;1277cc75-264e-80b8-a682-fbe937567d02&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;TDD는 소프트웨어 개발 방법론 중 하나로, 테스트를 먼저 작성한 후에 그 테스트를 통과하는 코드를 작성하는 방식이다. &lt;b&gt;코드를 작성하기 전에 먼저 해당 코드가 어떻게 동작해야 할지를 테스트로 정의하는 개발 프로세스이다. &lt;/b&gt;&lt;/p&gt;
&lt;h3 id=&quot;1277cc75-264e-80cc-a6cb-ccdd86bb7e1c&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;TDD 기본 싸이클&lt;/h3&gt;
&lt;ol id=&quot;1277cc75-264e-80f1-9637-dd8c36592924&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Red (실패하는 테스트 작성)&lt;/b&gt;
&lt;ul id=&quot;1277cc75-264e-80f7-a05f-fb417b212e6e&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;구현하려는 기능에 대한 테스트 코드를 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1277cc75-264e-8062-93c8-fcc7e41e186b&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;테스트가 실패하는 것을 확인함으로써, 아직 해당 기능이 없다는 것을 명확하게 인지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;1277cc75-264e-8032-bb8d-f222a3bfc6ff&quot; class=&quot;code reasonml&quot;&gt;&lt;code&gt;@Test
void testAdd() {
    Calculator calc = new Calculator();
    assertEquals(5, calc.add(2, 3));  // 아직 add 메서드가 구현되지 않음.
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol id=&quot;1277cc75-264e-8082-a924-d32359d1fe5b&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Green (테스트 통과를 위한 최소한의 코드 작성)&lt;/b&gt;
&lt;ul id=&quot;1277cc75-264e-8056-8caa-eff0c17a17dc&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;테스트를 통과시키기 위해 필요한 최소한의 코드를 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1277cc75-264e-8038-a069-ee88a9c122d6&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;코드를 반복적으로 실행 후, 테스트가 통과되도록 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;1277cc75-264e-8092-987a-c68c75846f4e&quot; class=&quot;code angelscript&quot;&gt;&lt;code&gt;public class Calculator {
    public int add(int a, int b) {
        return a + b;  // 테스트 통과를 위한 간단한 코드 작성
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol id=&quot;1277cc75-264e-8010-9204-d436f5e9386f&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Refactor (리팩토링)&lt;/b&gt;
&lt;ul id=&quot;1277cc75-264e-809b-948d-c11584d34cc7&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;중복 제거하거나, 가독성이나 성능을 개선하는 등 더 나은 코드 구조로 리팩토링.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul id=&quot;1277cc75-264e-80dc-9dd5-e870f69d65c2&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;리팩토링 후에도 테스트가 여전히 통과하는지 확인하여, 기능이 깨지지 않았음을 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;1277cc75-264e-8044-910c-fcba36ab90c2&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;TDD의 장점&lt;/h3&gt;
&lt;ol id=&quot;1277cc75-264e-80c5-b52c-c3e1dd3e8384&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;높은 코드 품질&lt;/b&gt;:개발자는 기능에 대한 명한 요구 사항을 이해하고 이를 테스트할 수 있는 구조로 코드를 작성하게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정적인 리팩토링 &lt;/b&gt;: 리팩토링을 통해 코드의 구조를 개선할 때, 테스트 코드가 기능의 정상 동작을 보장해 주기 때문에 기능이 손상되지 않은 상태에서 안전하게 코드를 개선할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 나은 설계&lt;/b&gt;: TDD를 사용하면 테스트 가능한 코드를 작성해야 하기 때문에 자연스럽게 의존성을 줄이고 모듈화된 설계로 이어진다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;1277cc75-264e-80e0-9eac-cd4d4ccb9060&quot; class=&quot;&quot; data-ke-size=&quot;size23&quot;&gt;TDD 단점&lt;/h3&gt;
&lt;ul id=&quot;1277cc75-264e-806e-b549-eec6d8f9b884&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&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;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;테스트 유지보수 비용 증가&lt;/b&gt;: 기능이 변화할 때마다 테스트 코드를 유지보수해야 한다. 추가적인 비용 발생 가능&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;잘못된 테스트 설계 위험&lt;/b&gt;: 테스트 설계를 잘못하거나, 지나치게 구현에 의존적인 테스트를 작성하면 리팩토링 시 유지 보수 비용 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;1267cc75-264e-80db-89b5-f87e2353d471&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;BDD&lt;/h2&gt;
&lt;p id=&quot;1277cc75-264e-8053-9d70-d3fcffce1e33&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;BDD는 비즈니스 요구 사항을 기반으로 사용자의 동작을 정의하고, 그 동작에 대한 테스트를 작성하는 방식이다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-806d-890b-eec0d4600d96&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;주로 &lt;b&gt;Given-When-Then&lt;/b&gt; 패턴을 사용&lt;/p&gt;
&lt;ul id=&quot;1277cc75-264e-80d7-b883-d996a7e5159e&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;Given&lt;/b&gt;: 시스템의 초기 상태(상황, 전제 조건)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;When&lt;/b&gt;: 사용자가 시스템에 취하는 행동(액션)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;Then&lt;/b&gt;: 그 행동에 대한 기대 결과(결과)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;1277cc75-264e-80d8-8980-ea7f4fb61ad9&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;BDD와 TDD&lt;/h2&gt;
&lt;p id=&quot;1277cc75-264e-80da-bd1f-d483b109f143&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 케이스를 작성하는 데는 시간과 비용이 많이 들어가지만, &lt;b&gt;이미 작성된 요구사항이나 기획서&lt;/b&gt;를 기반으로 테스트 케이스를 작성하면 이 비용을 줄일 수 있다. BDD와 TDD는 상호 배타적인 관계가 아니라 &lt;b&gt;상호 보완적인 관계이다&lt;/b&gt;. 프로젝트에서 &lt;b&gt;BDD&lt;/b&gt;를 통해 시나리오를 검증하고, &lt;b&gt;TDD&lt;/b&gt;를 통해 그 시나리오에 사용되는 각 모듈을 검증하는 방식이 효과적이다.&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-8071-803b-f3d91fa55c95&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;1267cc75-264e-8017-aa2a-e32a3f2daa8f&quot; class=&quot;&quot; data-ke-size=&quot;size26&quot;&gt;개발 주기 내에서 테스트하기&lt;/h2&gt;
&lt;ol id=&quot;1267cc75-264e-80d0-9fd7-db774c816e22&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;1&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개발&lt;/b&gt;
&lt;ul id=&quot;1287cc75-264e-806f-860e-e7444be5f5f0&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;개발자의 작업 장소&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;SCM에 여러번 커밋한다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;개발 단계에서 비즈니스 로직에 대해 &lt;b&gt;단위 테스트&lt;/b&gt;를 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol id=&quot;1277cc75-264e-80a1-a6b2-f8993177939b&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;통합 단계&lt;/b&gt;
&lt;ul id=&quot;1287cc75-264e-80dc-9101-f13804974391&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;모든 컴포넌트 포함하여 애플리케이션 빌드&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;자동화된 빌드를 수행하여 애플리케이션을 패키징하는 단계이다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;단위 테스트와 기능 테스트 수행. 기능 테스트는 블랙박스 테스트로 실행&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;시스템 몇몇 요소가 빠져있어 일부 테스트만 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol id=&quot;1277cc75-264e-8014-92ce-d1f861842ae9&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인수 단계, 부하 테스트 단계&lt;/b&gt;
&lt;ul id=&quot;1287cc75-264e-8033-b608-c12efeed6453&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;부하 테스트 : 애플리케이션에 부하 주어 적절하게 확장하는지 확인&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;인수 단계 : 고객이 시스템을 인수하는 단계&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;통합 단계와 동일한 테스트 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ol id=&quot;1287cc75-264e-80ad-8431-f8a2d5b2481d&quot; class=&quot;numbered-list&quot; style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; type=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;예비 운영&lt;/b&gt;
&lt;ul id=&quot;1287cc75-264e-80dd-ba83-c92ec9e4c1b9&quot; class=&quot;bulleted-list&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;실제 운영 배포 직전 수행하는 마지막 검증 단계&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;인수 단계에서 실행한 테스트 실행하는 것 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p id=&quot;1277cc75-264e-802f-ae73-ee28cc68a4f8&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;1267cc75-264e-8017-a7cc-c693c7472890&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;자료출처&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-806d-b671-d43c736dd69f&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tv.kakao.com/channel/3693125/cliplink/414004682&quot;&gt;https://tv.kakao.com/channel/3693125/cliplink/414004682&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80f9-bb5f-dd4c4f57b240&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@kimyj1234/TDD-BDD#bdd-%EA%B8%B0%EB%B3%B8-%ED%8C%A8%ED%84%B4&quot;&gt;https://velog.io/@kimyj1234/TDD-BDD#bdd-기본-패턴&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-80a7-8692-dac48591259c&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.wakmusic.xyz/tdd-vs-bdd-c738b507930f&quot;&gt;https://blog.wakmusic.xyz/tdd-vs-bdd-c738b507930f&lt;/a&gt;&lt;/p&gt;
&lt;p id=&quot;1277cc75-264e-804b-bed5-ea1b8539f74d&quot; class=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yozm.wishket.com/magazine/detail/2471/&quot;&gt;https://yozm.wishket.com/magazine/detail/2471/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/article&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Test</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/37</guid>
      <comments>https://youbin2.tistory.com/37#entry37comment</comments>
      <pubDate>Sun, 10 Nov 2024 22:53:22 +0900</pubDate>
    </item>
    <item>
      <title>[헤드퍼스트 디자인패턴] 프록시 패턴</title>
      <link>https://youbin2.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 패턴을 알아보자.&lt;/p&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;프록시 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프록시 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변하는 객체)을 제공한다. 프록시 패턴은 실제 객체에 대한 접근을 제어하고, 추가적인 기능(로깅, 캐싱, 접근 제어 등)을 제공할 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;프록시 패턴 개념과 목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 패턴은 실제 객체와 그 객체의 대리자 역할을 하는 객체(프록시 객체) 사이에 인터페이스를 정의하여, 클라이언트가 직접 실제 객체에 접근하지 않고, 프록시 객체를 통해 접근하도록 만든다. 프록시 객체는 클라이언트의 요청을 실제 객체로 전달하고, 그 결과를 클라이언트에게 반환하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 패턴의 주요 목적은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;접근 제어&lt;/b&gt;: 클라이언트가 실제 객체에 접근하기 전에 특정 조건을 확인하거나, 특정 사용자의 접근을 제한할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 초기화(Lazy Initialization)&lt;/b&gt;: 실제 객체의 생성이 비용이 많이 드는 경우, 필요할 때까지 객체 생성을 지연시킬 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로깅 및 감사(Auditing)&lt;/b&gt;: 객체에 대한 모든 접근이나 요청을 기록할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원격 프록시(Remote Proxy)&lt;/b&gt;: 실제 객체가 다른 주소 공간(예: 원격 서버)에 있는 경우, 클라이언트는 로컬 프록시를 통해 원격 객체에 접근할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱(Caching)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: 결과를 캐싱하여 동일한 요청에 대해 빠른 응답을 제공한다.&lt;/span&gt;&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;프록시 패턴은 다양한 유형으로 나눌 수 있으며, 각 유형은 특정 목적을 위해 사용된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;가상 프록시 (Virtual Proxy)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체의 생성 비용이 높을 때 사용된다. 프록시가 실제 객체의 생성을 지연시켜, 필요할 때까지 생성하지 않도록 한다.&lt;/li&gt;
&lt;li&gt;예: 이미지 뷰어에서 이미지를 표시할 때, 이미지가 실제로 필요한 순간까지 로드를 지연시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원격 프록시 (Remote Proxy)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체가 다른 주소 공간(네트워크 상의 원격 서버)에 있을 때 사용된다.&lt;/li&gt;
&lt;li&gt;클라이언트는 로컬 프록시를 통해 원격 객체에 접근하고, 원격 호출을 통해 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;예: 분산 객체 시스템에서, 클라이언트는 원격 서버의 객체를 로컬 프록시로 호출할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보호 프록시 (Protection Proxy)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체에 대한 접근을 제한할 때 사용된다.&lt;/li&gt;
&lt;li&gt;프록시는 특정 사용자나 클라이언트의 권한을 확인하고, 권한이 없는 경우 접근을 거부할 수 있다.&lt;/li&gt;
&lt;li&gt;예: 관리자가 아닌 사용자가 특정 파일을 삭제하지 못하도록 보호 프록시를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스마트 프록시 (Smart Proxy)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체에 대한 접근 전에 추가 작업을 수행하는 프록시.&lt;/li&gt;
&lt;li&gt;예: 실제 객체의 메서드를 호출하기 전에 로깅, 인증, 캐싱, 원격 호출 처리 등을 수행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;프록시 패턴 클래스 다이어그램 및 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 패턴 클래스 다이어그램은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XttVj/btsJkbqKU5q/Q0c7qgzUUX6K1Wh4Ucg1Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XttVj/btsJkbqKU5q/Q0c7qgzUUX6K1Wh4Ucg1Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XttVj/btsJkbqKU5q/Q0c7qgzUUX6K1Wh4Ucg1Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXttVj%2FbtsJkbqKU5q%2FQ0c7qgzUUX6K1Wh4Ucg1Ck%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;391&quot; height=&quot;181&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;181&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;Proxy와 RealSubject 모두 Subject 인터페이스를 구현한다. 이러면 어떤 클라이언트에서든 프록시를 주제와 똑같은 식으로 다룰 수 있다. RealSubject는 진짜 작업을 대부분 처리하는 개게이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy는 그 객체로의 접근을 제어하는 객체이다. Proxy에는 진짜 작업을 처리하는 객체의 레퍼런스가 들어있다. 진짜 객체가 필요하면 그 레퍼런스를 사용해 요청을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy에서 RealSubject의 인스턴스를 생성하거나, 그 객체의 생성 과정에 관여하는 경우가 많다.&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;위 클래스 다이어그램을 참고해서 간단한 Proxy 예제를 만들어보겠다. 가상 프록시를 구현한 예제이다.&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;다음과 같이 Subject 인터페이스를 정의한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724947570289&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Image {
    void display();
}&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;RealSubject를 정의한다. Subject를 상속받고 오버라이드를 해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724947592627&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println(&quot;Loading &quot; + fileName);
    }

    @Override
    public void display() {
        System.out.println(&quot;Displaying &quot; + fileName);
    }
}&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;Proxy 객체를 정의한다. 마찬가지로 Subject를 상속받아야 하며 RealSubject의 레퍼런스가 들어있다. 클라이언트 요청이 오면 진짜 객체의 레퍼런스를 사용해 요청을 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724947655867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProxyImage implements Image {
    private RealImage realImage;
    private String fileName;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName); // 실제 객체를 생성 (지연 초기화)
        }
        realImage.display(); // 실제 객체의 메서드 호출
    }
}&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;pre id=&quot;code_1724947758461&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProxyPatternExample {
    public static void main(String[] args) {
        Image image = new ProxyImage(&quot;test_image.jpg&quot;);

        // 이미지가 처음 요청될 때만 실제 객체를 생성하여 로드합니다.
        image.display(); // 로딩 후 표시
        System.out.println(&quot;&quot;);

        // 이후에는 로드된 이미지를 사용합니다.
        image.display(); // 바로 표시
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;동적 프록시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동적 프록시(dynamic proxy)&lt;/b&gt;는 자바의 javja.lang.reflect 패키지를 활용한다. 이 패키지를 사용하면 즉석에서 하나 이상의 인터페이스를 구현하고 지정한 클래스에 메소드 호출을 전달하는 프록시 클래스를 만들 수 있다. Java의 리플렉션(Reflection) API를 사용하여 인터페이스를 구현하는 프록시 객체를 생성하며, 개발자는 프록시 객체의 메서드 호출을 처리하는 로직을 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 프록시 클래스 다이어그램은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnw5vV/btsJkmy08G1/6YtpxBFnGtbaG8DjAjq8vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnw5vV/btsJkmy08G1/6YtpxBFnGtbaG8DjAjq8vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnw5vV/btsJkmy08G1/6YtpxBFnGtbaG8DjAjq8vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnw5vV%2FbtsJkmy08G1%2F6YtpxBFnGtbaG8DjAjq8vk%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;591&quot; height=&quot;211&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 동적 프록시는 다음 두 가지 주요 구성 요소를 사용한다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;java.lang.reflect.Proxy 클래스&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적 프록시 객체를 생성하는 데 사용되는 Java 표준 클래스.&lt;/li&gt;
&lt;li&gt;newProxyInstance() 메서드를 사용하여 런타임에 프록시 객체를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;java.lang.reflect.InvocationHandler 인터페이스&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 객체의 메서드 호출을 처리하는 인터페이스이다.&lt;/li&gt;
&lt;li&gt;invoke() 메서드를 구현하여 프록시 객체의 메서드 호출을 가로채고, 호출 전에/후에 추가 로직을 정의할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 Proxy 클래스를 생성해 주므로 Proxy 클래스에게 무슨 일을 해야 하는지 알려줘야 한다. 필요한 코드를 프록시 클래스에 넣을 수 없기 때문에 InvocationHandler에 넣어야 한다. InvocationHandler는 프록시에 호출되는 모든 메서드에 응답한다. Proxy에 메서드 호출을 받으면 항상 InvocationHandler에 진짜 작업을 부탁한다고 생각하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 간단한 예제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subject 인터페이스를 선언한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Subject {
    public void request();
}&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;RealSubject를 구현한다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class RealSubject implements Subject {

    @Override
    public void request() {
        System.out.println(&quot;request method called&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;InvocationHandler를 구현한다. InvocationHandler는 Java 내부에 선언되어 있기 때문에 따로 만들 필요 없다. InvocationHandler는 invoke(Object proxy, Method method, Object[] args) 메서드만 갖고 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class InvocationHandlerImpl implements InvocationHandler {
    private Object target;

    public InvocationHandlerImpl(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(&quot;메서드 호출 전: &quot; + method.getName()); // 메서드 호출 전 로그
        Object result = method.invoke(target, args); // 실제 객체의 메서드 호출
        System.out.println(&quot;메서드 호출 후: &quot; + method.getName()); // 메서드 호출 후 로그
        return result;
    }
}&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;pre class=&quot;java&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.lang.reflect.Proxy;

public class ProxyTest {
    public static void main(String[] args) {
        Subject subject = new RealSubject();

        Subject proxySubject = (Subject) Proxy.newProxyInstance(
                subject.getClass().getClassLoader(),
                subject.getClass().getInterfaces(),
                new InvocationHandlerImpl(subject)
        );

        proxySubject.request();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AZ6rZ/btsJlbDnVNt/dfx9XDICaD5qVWlKciiSDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AZ6rZ/btsJlbDnVNt/dfx9XDICaD5qVWlKciiSDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AZ6rZ/btsJlbDnVNt/dfx9XDICaD5qVWlKciiSDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAZ6rZ%2FbtsJlbDnVNt%2Fdfx9XDICaD5qVWlKciiSDk%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;582&quot; height=&quot;216&quot; data-origin-width=&quot;582&quot; data-origin-height=&quot;216&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;</description>
      <category>디자인패턴</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/36</guid>
      <comments>https://youbin2.tistory.com/36#entry36comment</comments>
      <pubDate>Fri, 30 Aug 2024 01:27:45 +0900</pubDate>
    </item>
    <item>
      <title>[헤드퍼스트 디자인패턴] 상태 패턴</title>
      <link>https://youbin2.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;상태 패턴을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;상태 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 상태가 바뀜에 따라 객체의 행동이 바뀔 수 있도록 해준다. 마치 객체의 클래스가 바뀌는 것 같은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;요구사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뽑기 기계가 있다고 가정해 보자. 뽑기 기계에는 &lt;b&gt;'동전이 없는 상태(초기 상태)', '동전이 있는 상태', '뽑기가 나온 상태', '뽑기가 매진된 상태'&lt;/b&gt;가 있을 것이다. 또한 &lt;b&gt;'동전 투입', 동전 반환', '손잡이 돌림', '알맹이 내보냄'&lt;/b&gt;과 같이 4가지 상태를 바꾸는 행동도 있다. 이것을 프로그래밍으로 어떻게 구현해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 현재 상태를 저장하는 인스턴스 변수를 만들고 각 상태의 값을 정의한다고 가정해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1724682407442&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	// 뽑기 매진 상태
    final static int SOLD_OUT = 0;
    // 동전이 없는 상태
    final static int NO_QUARTER = 1;
    // 동전이 들어간 상태
    final static int HAS_QUATER = 2;
    // 뽑기 판매 상태
    final static int SOLD = 3;&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가지 상태에 따라 &lt;b&gt;동전 투입 메서드(insertCoin()), 동전 반환 메서드(ejectQuater()), 손잡이 돌리는 메서드(turnCrank()), 뽑기 내보내는 메서드(produce())&lt;/b&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;insertCoin() 메서드는 다음과 같을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1724682764944&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	// 뽑기 매진 상태
    final static int SOLD_OUT = 0;
    // 동전이 없는 상태
    final static int NO_QUARTER = 1;
    // 동전이 들어간 상태
    final static int HAS_QUATER = 2;
    // 뽑기 판매 상태
    final static int SOLD = 3;

    private int state = SOLD_OUT;

    public void insertCoin() {
        if(state == HAS_QUATER) {
            System.out.println(&quot;You have already inserted coins.&quot;);
        } else if(state == NO_QUARTER) {
            state = HAS_QUATER;
            System.out.println(&quot;coin inserted&quot;);
        } else if(state == SOLD_OUT) {
            System.out.println(&quot;already sold out&quot;);
        } else if(state == SOLD) {
            System.out.println(&quot;The product is coming out&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;위 코드처럼 조건문을 통해 상태를 변경하는 방법은 다음 4가지 문제를 고려하지 않고 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드는 OCP를 지키고 있지 않고 있다.&lt;/li&gt;
&lt;li&gt;상태 전환이 복잡한 조건문 속에 숨어 있어 분명하게 드러나지 않고 있다.&lt;/li&gt;
&lt;li&gt;바뀌는 부분을 캡슐화하지 않고 있다.&lt;/li&gt;
&lt;li&gt;새로운 기능을 추가하는 과정이 복잡하며, 기존에 없던 새로운 버그가 생길 가능성이 높다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 &lt;b&gt;상태 패턴&lt;/b&gt;을 도입해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;상태 패턴 클래스 다이어그램 및 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태패턴이란 내부&lt;/b&gt; &lt;b&gt;상태가 바뀜에 따라 객체의 행동이 바뀔 수 있도록 해준다. 마치 객체의 클래스가 바뀌는 것 같은 결과를 얻을 수 있다.&lt;/b&gt; 이 패턴은 객체의 상태를 캡슐화하고, 그 상태에 따른 행동을 각 상태 객체로 옮겨서 관리하는 방식으로 구현된다. 상태 패턴을 사용하면 상태 전환 로직을 상태별 클래스 안으로 캡슐화하여 코드의 유연성과 유지 보수성을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 다이어그램은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPBFgz/btsJe9mMTaV/PIVQYq9FOnhAAge55c14t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPBFgz/btsJe9mMTaV/PIVQYq9FOnhAAge55c14t1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPBFgz/btsJe9mMTaV/PIVQYq9FOnhAAge55c14t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPBFgz%2FbtsJe9mMTaV%2FPIVQYq9FOnhAAge55c14t1%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;461&quot; height=&quot;181&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 다이어그램을 참고하여 코드로 구현하도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724685025807&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package state;

public abstract class State {
    public abstract void insertCoin();
    public abstract void ejectCoin();
    public abstract boolean turncrank();
    public abstract void produce();
}&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;State를 구현한 &lt;b&gt;NoCoinState, OneCoinState, ProduceState, SoldoutState&lt;/b&gt;는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724769449515&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class NoCoinState extends State {
    private Context context;

    public NoCoinState(Context context) {
        this.context = context;
    }

    @Override
    public void insertCoin() {
        System.out.println(&quot;you inserted a coin&quot;);
        context.setState(context.getOneCoinState());
    }

    @Override
    public void ejectCoin() {
        System.out.println(&quot;no coin to eject&quot;);
    }

    @Override
    public boolean turncrank() {
        System.out.println(&quot;no coin. please insert a coin&quot;);
        return false;
    }

    @Override
    public void produce() {
        System.out.println(&quot;no coin. please insert a coin&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724769475866&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OneCoinState extends State {
    private Context context;

    public OneCoinState(Context context) {
        this.context = context;
    }

    @Override
    public void insertCoin() {
        System.out.println(&quot;You have already inserted a coin.&quot;);
    }

    @Override
    public void ejectCoin() {
        System.out.println(&quot;eject coin.&quot;);
        context.setState(context.getNoCoinState());
    }

    @Override
    public boolean turncrank() {
        System.out.println(&quot;turn crank&quot;);
        context.setState(context.getProduceState());
        return true;
    }

    @Override
    public void produce() {
        System.out.println(&quot;you can't produce product now&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724769497220&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ProduceState extends State {
    Context context;

    public ProduceState(Context context) {
        this.context = context;
    }

    @Override
    public void insertCoin() {
        System.out.println(&quot;producing now..&quot;);
    }

    @Override
    public void ejectCoin() {
        System.out.println(&quot;you can't eject a coin. producing now..&quot;);
    }

    @Override
    public boolean turncrank() {
        System.out.println(&quot;producing now..&quot;);
        return false;
    }

    @Override
    public void produce() {
        context.reduceProduceCount();
        if(context.getProduceCount() &amp;gt; 0) {
            context.setState(context.getNoCoinState());
        } else if(context.getProduceCount() == 0) {
            context.setState(context.getSoldoutState());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724769525784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SoldOutState extends State {
    private Context context;

    public SoldOutState(Context context) {
        this.context = context;
    }

    @Override
    public void insertCoin() {
        System.out.println(&quot;sold out&quot;);
    }

    @Override
    public void ejectCoin() {
        System.out.println(&quot;you can't eject coin&quot;);
    }

    @Override
    public boolean turncrank() {
        System.out.println(&quot;sold out&quot;);
        return false;
    }

    @Override
    public void produce() {
        System.out.println(&quot;sold out&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개의 클래스 모두 각자 상황에 맞춰 State 클래스 안에 있는 메서드를 오버라이드 하고 있다. 또한 공통적으로 Context를 구성하고 생성자에서 인스턴스를 받고 있다. context는 나중에 다른 상태로 전환할 때 필요한 레퍼런스이다.&amp;nbsp;&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;Context 클래스는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724769627577&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Context {
    private State soldoutState;
    private State noCoinState;
    private State oneCoinState;
    private State produceState;
    private State randomPlusCountState;

    private State state;
    private int produceCount;

    public Context(int produceCount) {
        this.produceCount = produceCount;

        if(produceCount &amp;gt; 0) {
            state = noCoinState;
        } else {
            state = soldoutState;
        }

        soldoutState = new SoldOutState(this);
        noCoinState = new NoCoinState(this);
        oneCoinState = new OneCoinState(this);
        produceState = new ProduceState(this);
        randomPlusCountState = new RandomPlusCountState(this);
    }

    public void insertCoin() {
        state.insertCoin();
    }
    public void ejectCoin() {
        state.ejectCoin();
    }
    public void turnCrank() {
        if(state.turncrank()) {
            state.produce();
        }
    }

    public boolean reduceProduceCount() {
        if(produceCount &amp;lt;= 0) {
            return false;
        }
        produceCount--;
        return true;
    }

    public State getSoldoutState() {
        return soldoutState;
    }

    public State getNoCoinState() {
        return noCoinState;
    }

    public State getOneCoinState() {
        return oneCoinState;
    }

    public State getProduceState() {
        return produceState;
    }

    public State getRandomPlusCountState() {
        return randomPlusCountState;
    }

    public int getProduceCount() {
        return produceCount;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void setProduceCount(int produceCount) {
        this.produceCount = produceCount;
    }
}&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가지 효과를 기대할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 상태의 행동을 별개의 클래스로 국지화했다.&lt;/li&gt;
&lt;li&gt;관리하기 힘든 조건문을 없앴다.&lt;/li&gt;
&lt;li&gt;각 상태를 변경에는 닫혀있게 하고, State 클래스는 새로운 상태 클래스를 추가하는 확장에는 열려 있도록 했다.(OCP)&lt;/li&gt;
&lt;li&gt;다이어그램에 가까우면서 이해하기 좋은 클래스 구조를 생산해 냈다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 코드를 보면 구상 상태 클래스(State를 구현한 클래스)에서 다음 상태를 결정하고 있는데 항상 그래야 하는 건 아니다. Context에서 상태 전환 흐름을 결정할 수 있다. 상태 전환이 고정되어 있으면 상태 전환 흐름을 결정하는 코드를 Context에 넣어도 된다. 하지만 상태 전환이 동적으로 결정된다면 상태 클래스 내에 처리하는 것이 좋다.&amp;nbsp;예를 들어 State에서 NoCoinState 또는 SoldOutState로 전환하는 결정은 실행 중에 남아있는 produceCount에 의해 동적으로 결정될 수밖에 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 전환 코드를 상태 클래스에 넣으면 상태 클래스 사이에 의존성이 생기는 단점이 있다. State 구현 코드를 보면 구상 상태 클래스를 코드에 직접 넣는 대신 Context 객체의 Getter 메서드를 써서 의존성을 최소화하려고 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;상태 패턴 VS 전략 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 패턴과 전략 패턴의 클래스 다이어그램을 비교해 보면 비슷하다는 사실을 알게 된다. 상태 패턴을 사용할 때 상태 객체에 일련의 행동이 캡슐화된다. 상황에 따라 Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 맡기게 된다. 그 객체 내부 상태에 따라 현재 상태를 나타내는 객체가 바뀌게 되고, 그 결과로 Context 객체 행동오 바뀌게 된다. 중요한 점은 클라이언트는 상태 객체를 몰라도 된다는 것이다.&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;전략 패턴은 클라이언트가 Context 객체에게 어떤 전략 객체를 사용할지 지정한다. 전략 패턴은 주로 실행 시에 전략 객체를 변경할 수 있는 유연성을 제공한다. 일반적으로 전략 패턴은 서브클래스를 만드는 방법을 대신해서 유연성을 극대화하는 용도로 쓰인다. 상속을 사용해서 클래스의 행동을 정의하다 보면 행동을 변경할 때 마음대로 변경하기 힘들다. 하지만 전략 패턴을 사용하면 구성으로 행동을 정의하는 객체를 유연하게 바꿀 수 있다.&amp;nbsp;&lt;/p&gt;</description>
      <category>디자인패턴</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/35</guid>
      <comments>https://youbin2.tistory.com/35#entry35comment</comments>
      <pubDate>Tue, 27 Aug 2024 23:56:57 +0900</pubDate>
    </item>
    <item>
      <title>[헤드퍼스트 디자인패턴] 반복자 패턴과 컴포지트 패턴</title>
      <link>https://youbin2.tistory.com/34</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;반복자 패턴과 컴포지트 패턴을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;border-bottom: 3px solid #707070; font-weight: bold; padding: 5px;&quot; data-ke-size=&quot;size26&quot;&gt;반복자 패턴과 컴포지트 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반복자 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컴포지트 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 트리구조로 구성해서 부분-전체 계층구조를 구현한다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;H3 중제목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열, 리스트, 맵 자료구조를 각각 3개의 클래스에서 저장한다고 가정해 보자. Client라는 객체에서 배열, 리스트, 맵 3개를 모두 조회하려면 3개의 자료구조 형태에 맞춰 코드로 구현해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client가 배열, 리스트, 맵 등의 자료구조를 직접 조회해야 한다면, 클라이언트 코드에서 각각의 자료구조에 대해 다른 방식으로 요소에 접근해야 한다. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만든다. 아래는 이러한 문제를 나타내는 Java 코드 예시이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열 저장 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724255653814&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArrayData {
    private String[] data;

    public ArrayData(String[] data) {
        this.data = data;
    }

    public String[] getData() {
        return data;
    }
}&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;리스트 저장 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724255667110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

public class ListData {
    private List&amp;lt;String&amp;gt; data;

    public ListData(List&amp;lt;String&amp;gt; data) {
        this.data = data;
    }

    public List&amp;lt;String&amp;gt; getData() {
        return data;
    }
}&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;맵 저장 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724255694805&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Map;

public class MapData {
    private Map&amp;lt;String, String&amp;gt; data;

    public MapData(Map&amp;lt;String, String&amp;gt; data) {
        this.data = data;
    }

    public Map&amp;lt;String, String&amp;gt; getData() {
        return data;
    }
}&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;클라이언트 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724255724631&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ClientWithoutIteratorPattern {

    public static void main(String[] args) {
        // 배열 데이터
        String[] array = {&quot;Apple&quot;, &quot;Banana&quot;, &quot;Cherry&quot;};
        ArrayData arrayData = new ArrayData(array);

        // 리스트 데이터
        List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
        list.add(&quot;Dog&quot;);
        list.add(&quot;Elephant&quot;);
        list.add(&quot;Fox&quot;);
        ListData listData = new ListData(list);

        // 맵 데이터
        Map&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        map.put(&quot;Key1&quot;, &quot;Grapes&quot;);
        map.put(&quot;Key2&quot;, &quot;Honeydew&quot;);
        map.put(&quot;Key3&quot;, &quot;Iced Tea&quot;);
        MapData mapData = new MapData(map);

        // 배열 요소 조회
        System.out.println(&quot;Array Data:&quot;);
        for (int i = 0; i &amp;lt; arrayData.getData().length; i++) {
            System.out.println(arrayData.getData()[i]);
        }

        // 리스트 요소 조회
        System.out.println(&quot;\nList Data:&quot;);
        for (int i = 0; i &amp;lt; listData.getData().size(); i++) {
            System.out.println(listData.getData().get(i));
        }

        // 맵 요소 조회
        System.out.println(&quot;\nMap Data:&quot;);
        for (Map.Entry&amp;lt;String, String&amp;gt; entry : mapData.getData().entrySet()) {
            System.out.println(entry.getKey() + &quot; = &quot; + entry.getValue());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조는 여전히 클라이언트 코드가 각 자료구조의 내부 구현에 의존하고 있으며, 자료구조가 추가되거나 변경될 때마다 클라이언트 코드를 수정해야 하는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 반복자 패턴이 만들어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;반복자 패턴 클래스 다이어그램 및 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반복자 패턴(Iterator Pattern)&lt;/b&gt;은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공한다. 반복자 패턴을 사용하면 집합체 내에서 어떤 식으로 일이 처리되는지 전혀 모르는 상태에서 그 안에 들어있는 모든 항목을 대상으로 반복 작업을 수행할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복자 패턴 클래스 다이어그램은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byOZqD/btsJbwI7hXX/MutYgrO8kOfLxkKJANSYcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byOZqD/btsJbwI7hXX/MutYgrO8kOfLxkKJANSYcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byOZqD/btsJbwI7hXX/MutYgrO8kOfLxkKJANSYcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyOZqD%2FbtsJbwI7hXX%2FMutYgrO8kOfLxkKJANSYcK%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;581&quot; height=&quot;311&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client 입장에서 Iterator 인터페이스에만 의존하고 있기 때문에 Iterator 객체만 있으면 배열로 저장되어 있든 ArrayList로 저장되어 있든 신경 쓰지 않고 작업을 처리할 수 있다.&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;반복자 패턴 클래스 다이어그램을 참고하여 코드로 구현해 보았다.&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;Aggregate 인터페이스이다. createIterator()에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Iterator를 반환해야 한다. Iterator 인터페이스는 직접 구현하지 않고, Java에 기본적으로 내장된 인터페이스를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724336401340&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Iterator;

public interface Aggregate {
    public Iterator&amp;lt;MyItem&amp;gt; createIterator();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyItem 클래스를 Aggregate를 구현한 객체(ConcreteAggregateA, ConcreteAggregateB, ConcreteAggregateC)에서 각자 다른 자료구조로 갖고 있게 된다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ConcreteAggregateA,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ConcreteAggregateB,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ConcreteAggregateC 모두 Aggregate 인터페이스를 구현했기 때문에 createIterator()를 오버라이드 해 iterator를 반환해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724336474578&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyItem {
    private String name;
    private String description;

    public MyItem(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724336523435&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ConcreteAggregateA implements Aggregate {
    private MyItem[] myItems;
    private final int MAX_VALUE = 10;
    private int position = 0;

    public ConcreteAggregateA() {
        myItems = new MyItem[MAX_VALUE];
    }

    public void add(String name, String description) {
        if(position &amp;gt;= MAX_VALUE) return;
        myItems[position] = new MyItem(name, description);
        position++;
    }

    @Override
    public Iterator&amp;lt;MyItem&amp;gt; createIterator() {
        return new ConcreteAIterator(myItems);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724336538654&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ConcreteAggregateB implements Aggregate {
    List&amp;lt;MyItem&amp;gt; myItemList;

    public ConcreteAggregateB() {
        myItemList = new ArrayList&amp;lt;&amp;gt;();
        myItemList.add(new MyItem(&quot;hihi&quot;, &quot;hello hello&quot;));
        myItemList.add(new MyItem(&quot;name&quot;, &quot;descriptioin&quot;));
    }

    @Override
    public Iterator&amp;lt;MyItem&amp;gt; createIterator() {
        return myItemList.iterator();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724336554358&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ConcreteAggregateC implements Aggregate {
    Map&amp;lt;String, MyItem&amp;gt; map;

    public ConcreteAggregateC() {
        this.map = new HashMap&amp;lt;&amp;gt;();

        map.put(&quot;hihi&quot;, new MyItem(&quot;map&quot;, &quot;map description&quot;));
    }

    @Override
    public Iterator&amp;lt;MyItem&amp;gt; createIterator() {
        return map.values().iterator();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 중요한 점은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ConcreteAggregateA와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ConcreteAggregateB,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ConcreteAggregateC의 차이점이다.&lt;/p&gt;
&lt;p 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;141&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CSAq5/btsJcT32iUs/BGKrZEO1K2vmkR4JLo7wE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CSAq5/btsJcT32iUs/BGKrZEO1K2vmkR4JLo7wE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CSAq5/btsJcT32iUs/BGKrZEO1K2vmkR4JLo7wE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCSAq5%2FbtsJcT32iUs%2FBGKrZEO1K2vmkR4JLo7wE1%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;141&quot; height=&quot;191&quot; data-origin-width=&quot;141&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 Collection 인터페이스는 Iterable 인터페이스를 확장하며, 이로 인해 모든 컬렉션 클래스(ArrayList, LinkedList, HashSet 등)는 iterator() 메서드를 제공하고 있다. 따라서 Collection 인터페이스를 쓰고 있는 ConcreteAggregateB(List 사용), ConcreteAggregateC(Map 사용)은 iterator를 구현할 필요 없이 자신이 구현한&amp;nbsp;iterator() 메서드를 호출하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYSn3R/btsJcADKLfB/IAkpkA6B6d0CtS7Mx88v4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYSn3R/btsJcADKLfB/IAkpkA6B6d0CtS7Mx88v4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYSn3R/btsJcADKLfB/IAkpkA6B6d0CtS7Mx88v4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYSn3R%2FbtsJcADKLfB%2FIAkpkA6B6d0CtS7Mx88v4k%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;976&quot; height=&quot;360&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;360&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;이와 달리 ConcreteAggregateA의 배열(myItems)은 자체적으로 구현한 iterator를 반환하는 메서드가 없기 때문에 만들어줘야 한다. 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724337233019&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Iterator;

public class ConcreteAIterator implements Iterator&amp;lt;MyItem&amp;gt; {
    private MyItem[] myItems;
    int position = 0;

    public ConcreteAIterator(MyItem[] myItems) {
        this.myItems = myItems;
    }

    @Override
    public boolean hasNext() {
        if(myItems.length &amp;lt;= position || myItems[position] == null) {
            return false;
        }
        return true;
    }

    @Override
    public MyItem next() {
        MyItem myItem = myItems[position];
        position++;
        return myItem;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
}&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;마지막으로 Client 클래스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1724337312333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Client {
    public static void main(String[] args) {
        List&amp;lt;Aggregate&amp;gt; aggregates = new ArrayList&amp;lt;&amp;gt;();

        ConcreteAggregateA concreteAggregateA = new ConcreteAggregateA();
        concreteAggregateA.add(&quot;aa&quot;, &quot;aaa&quot;);
        concreteAggregateA.add(&quot;bb&quot;, &quot;bbb&quot;);

        aggregates.add(concreteAggregateA);
        aggregates.add(new ConcreteAggregateB());
        aggregates.add(new ConcreteAggregateC());

        Client client = new Client(aggregates);
        client.printAll();
    }

    private List&amp;lt;Aggregate&amp;gt; aggregateList;
    public Client(List&amp;lt;Aggregate&amp;gt; aggregateList) {
        this.aggregateList = aggregateList;
    }

    public void printAll() {
        Iterator&amp;lt;Aggregate&amp;gt; aggregateIterator = aggregateList.iterator();
        while(aggregateIterator.hasNext()) {
            Aggregate next = aggregateIterator.next();
            print(next.createIterator());
        }
    }

    private void print(Iterator&amp;lt;MyItem&amp;gt; iterator) {
        while(iterator.hasNext()) {
            MyItem myItem = iterator.next();
            System.out.println(&quot;name : &quot; + myItem.getName() + &quot;, description : &quot; + myItem.getDescription());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;단일 역할 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집합체에서 내부 컬렉션 관련 기능과 반복자용 메서드 관련 기능을 전부 구현한다면 컬렉션이 변경되거나, 반복자 관련 기능이 바뀌었을 때 클래스가 바뀌어야 한다. 이러한 설계는 &lt;b&gt;단일 역할 원칙&lt;/b&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;&quot;클래스는 하나의 책임만 가져야 한다&quot;&lt;/b&gt;는 것을 의미한다. 더 쉽게 설명하자면 &lt;b&gt;클래스가 변경되는 이유는 오직 하나여야 한다는 원칙&lt;/b&gt;이다. 이는 클래스가 하나의 기능만을 수행하도록 설계되어야 한다는 것을 강조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 클래스에서 맡고 있는 모든 역할은 나중에 코드 변화를 불러올 수 있다. 역할이 2개 이상 있으면 바뀔 수 있는 부분은 2개 이상 되는 것이다. 이 원칙에 따라 하나의 클래스는 하나의 역할만 맡아야 한다.&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;간단한 예시를 들어보겠다. 직원의 정보를 관리하고, 월급을 계산하며, 그 데이터를 파일로 저장하는 시스템을 개발하면 Employee 클래스는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724338002259&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // 직원의 데이터를 가져오는 메서드
    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    // 월급을 계산하는 메서드
    public double calculateMonthlySalary() {
        return salary / 12;
    }

    // 직원 데이터를 파일로 저장하는 메서드
    public void saveToFile() {
        // 파일 저장 로직
    }
}&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;직원의 기본 정보 관리 (name, salary 등의 필드와 getter 메서드).&lt;/li&gt;
&lt;li&gt;월급 계산 (calculateMonthlySalary 메서드).&lt;/li&gt;
&lt;li&gt;데이터를 파일로 저장 (saveToFile 메서드).&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &quot;이 클래스가 왜 변경될 수 있을까?&quot;를 생각해 보자:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;급여 계산 방법이 변경&lt;/b&gt;: 예를 들어, 회사에서 새로운 급여 계산 규칙을 도입할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 저장 형식이 변경될 수 있다&lt;/b&gt;: 데이터를 파일로 저장할 때, 예를 들어, 파일 형식을 CSV에서 JSON으로 변경해야 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직원의 기본 정보가 변경될 수 있다&lt;/b&gt;: 예를 들어, 직원의 주소나 전화번호 같은 추가 정보가 필요해질 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 클래스는 이 세 가지 이유 각각으로 인해 변경될 가능성이 있다. 이제 이 클래스의 한 부분이 변경되면, 의도하지 않게 다른 부분에 영향을 줄 수 있다. 예를 들어, 파일 저장 형식을 변경할 때, 급여 계산과 관련된 코드가 의도치 않게 수정될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해, 클래스를 각각의 변경 이유에 따라 분리해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 직원 정보 관리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724338143649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }
}&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;2. 급여 계산&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724338169555&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SalaryCalculator {
    public double calculateMonthlySalary(Employee employee) {
        return employee.getSalary() / 12;
    }
}&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;3. 파일 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724338196408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EmployeeFileSaver {
    public void saveToFile(Employee employee) {
        // 파일 저장 로직
    }
}&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;각 클래스는 하나의 책임&lt;/b&gt;만 가지고 있기 때문에, 하나의 기능을 변경할 때 다른 기능에 영향을 주지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;컴포지트 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음으로 컴포지트 패턴을 알아보도록 하겠다. &lt;b&gt;컴포지트 패턴&lt;/b&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d261EF/btsJbwI89oN/fcCP6KlZ3awVKuL8aEQ4P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d261EF/btsJbwI89oN/fcCP6KlZ3awVKuL8aEQ4P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d261EF/btsJbwI89oN/fcCP6KlZ3awVKuL8aEQ4P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd261EF%2FbtsJbwI89oN%2FfcCP6KlZ3awVKuL8aEQ4P1%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;499&quot; height=&quot;371&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴포지트 패턴의 주요 개념&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;구성 요소(Component)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Component는 모든 객체들이 가져야 하는 공통 인터페이스를 정의한다. 이 인터페이스에는 트리 구조의 개별 객체와 그 집합을 동일하게 다룰 수 있는 메서드들이 포함된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;잎(Leaf)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leaf는 트리 구조의 가장 작은 구성 요소로, 자식이 없는 노드이다. Leaf는 실제 작업을 수행하며, 일반적으로 재귀적으로 호출되는 메서드를 구현한다. Leaf는 더 이상 자식을 추가할 수 없는 가장 말단의 객체이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴포지트(Composite)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Composite는 자식 구성 요소(다른 Leaf 또는 Composite)를 포함할 수 있는 복합 객체이다. Composite는 자식 구성 요소를 관리하고, 트리 구조에서의 작업을 재귀적으로 수행하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 다이어그램을 참고하여 코드로 구현해 보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;컴포지트 패턴 코드 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Component 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1724341730228&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class Component {
    public void operation() {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getDescription() {
        throw new UnsupportedOperationException();
    }

    public void print() {
        throw new UnsupportedOperationException();
    }

    public void add(Component component) {
        throw new UnsupportedOperationException();
    }

    public void remove(Component component) {
        throw new UnsupportedOperationException();
    }

    public Component getChild(int idx) {
        throw new UnsupportedOperationException();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 메서드는 Leaf 클래스에서 어떤 메서드는 Composit 클래스에서 사용할 수 있기 때문에 모두 UnsupportedOperationException()으로 예외처리를 했다. 각 클래스는 자기 역할에 맞지 않는 케소드는 오버라이드 하지 않고 기본 구현을 사용하면 된다.&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;다음은 Leaf 클래스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1724341814541&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Leaf extends Component {
    private String name;
    private String description;

    public Leaf(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void print() {
        System.out.println(&quot;name : &quot; + name + &quot;, description : &quot; + description);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }
}&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;다음으로 Composit 클래스이다.&lt;/p&gt;
&lt;pre id=&quot;code_1724341858377&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

public class Composit extends Component {
    private String name;
    private String description;
    private List&amp;lt;Component&amp;gt; childList;

    public Composit(String name, String description) {
        this.name = name;
        this.description = description;
        childList = new ArrayList&amp;lt;&amp;gt;();
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void print() {
        System.out.println(&quot;-----composit-----&quot;);
        System.out.println(&quot;composit name : &quot; + name + &quot;, description : &quot; +description);
        for(Component child : childList) {
            child.print();
        }
    }

    @Override
    public void add(Component component) {
        childList.add(component);
    }

    @Override
    public void remove(Component component) {
        childList.remove(component);
    }

    @Override
    public Component getChild(int idx) {
        return childList.get(idx);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List&amp;lt;Component&amp;gt; childList에 자식을 저장할 수 있다.&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;다음으로 Client 클래스와 테스트 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1724341930284&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Client {
    private Component rootComponent;

    public Client(Component rootComponent) {
        this.rootComponent = rootComponent;
    }

    public void print() {
        rootComponent.print();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1724341944641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package compositePattern;

public class CompositeTest {
    public static void main(String[] args) {
        Composit rootComposit = new Composit(&quot;root composit&quot;, &quot;root&quot;);
        Client client = new Client(rootComposit);

        Composit child1 = new Composit(&quot;child1&quot;, &quot;child&quot;);
        Composit child2 = new Composit(&quot;child2&quot;, &quot;child&quot;);
        Composit child3 = new Composit(&quot;child3&quot;, &quot;child&quot;);
        Composit child4 = new Composit(&quot;child4&quot;, &quot;child&quot;);


        rootComposit.add(child1);
        rootComposit.add(child2);
        rootComposit.add(child3);
        child3.add(child4);

        child1.add(new Leaf(&quot;leafA1&quot;, &quot;hi&quot;));
        child1.add(new Leaf(&quot;leafA2&quot;, &quot;hihi&quot;));

        child2.add(new Leaf(&quot;leafB1&quot;, &quot;wow&quot;));
        child2.add(new Leaf(&quot;leafB2&quot;, &quot;wowowow&quot;));

        child3.add(new Leaf(&quot;leafC1&quot;, &quot;wowowow&quot;));
        child3.add(new Leaf(&quot;leafC2&quot;, &quot;wowowow&quot;));
        child3.add(new Leaf(&quot;leafC3&quot;, &quot;wowowow&quot;));

        child4.add(new Leaf(&quot;leafD3&quot;, &quot;wowowowowwowow&quot;));

        rootComposit.print();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckhOuC/btsJcNJLxls/t3iIhZ4dhVXuO4GvVdfBwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckhOuC/btsJcNJLxls/t3iIhZ4dhVXuO4GvVdfBwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckhOuC/btsJcNJLxls/t3iIhZ4dhVXuO4GvVdfBwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckhOuC%2FbtsJcNJLxls%2Ft3iIhZ4dhVXuO4GvVdfBwk%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;816&quot; height=&quot;778&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;font-weight: bold; border-bottom: 1px solid #d83c3c; margin: 10px 0px 5px; border-left: 5px solid #d83c3c; letter-spacing: -0.07em; line-height: 30px; padding: 0px 10px 1px;&quot; data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포지트 패턴과 단일 책임 원칙을 같이 배움으로써 &lt;b&gt;상황에 따라 원칙을 적절하게 사용해야 함을 알 수 있다&lt;/b&gt;. 컴포지트 패턴은 단일 책임 원칙을 깨고 있다. 컴포지트 패턴에서 Composite 클래스는 다음과 같은 여러 책임을 가질 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자식 요소 관리&lt;/b&gt;: 자식 요소를 추가하고, 제거하고, 접근하는 책임.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트리 구조 관리&lt;/b&gt;: 자식 요소의 순회를 관리하는 책임.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자신의 작업 수행&lt;/b&gt;: Leaf와 마찬가지로 자신의 작업(예: operation() 메서드에서 정의된 작업)을 수행하는 책임.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 설계에서 모든 원칙을 완벽하게 따르는 것은 어려울 수 있으며, 때로는 설계의 목표와 상황에 따라 특정 원칙을 일부 타협할 필요가 있다. 한 가지 원칙만 고수하는 것보다&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;상황에 따라 유연하게 접근하는 것이 더 좋은 설계를 이끌어낼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>디자인패턴</category>
      <author>hongyb</author>
      <guid isPermaLink="true">https://youbin2.tistory.com/34</guid>
      <comments>https://youbin2.tistory.com/34#entry34comment</comments>
      <pubDate>Fri, 23 Aug 2024 00:57:38 +0900</pubDate>
    </item>
  </channel>
</rss>