<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
    <link>https://soobarkbar.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 9 Jun 2026 10:49:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>기내식은수박바</managingEditor>
    <image>
      <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
      <url>https://tistory1.daumcdn.net/tistory/3076242/attach/d9ec458535e6467b967018217189ee98</url>
      <link>https://soobarkbar.tistory.com</link>
    </image>
    <item>
      <title>Redis가 정말 싱글 쓰레드로 작동할까?</title>
      <link>https://soobarkbar.tistory.com/259</link>
      <description>&lt;p&gt;Redis는 클라이언트 요청을 이벤트 루프 기반으로 처리하고, Redis 자료구조를 직접 읽고 쓰는 명령 실행은 메인 쓰레드 중심으로 동작한다. 하지만 파일 닫기, AOF fsync, 메모리 해제 같은 일부 느린 작업은 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;p&gt;그래서 Redis를 정확히 이해하려면 다음 두 가지를 구분해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Redis 명령 실행 경로
   - GET, SET, INCR, HSET 같은 명령 실행
   - 메인 쓰레드 중심

2. Redis 프로세스 전체
   - 메인 쓰레드 외에도 백그라운드 쓰레드 존재
   - 파일 닫기, AOF fsync, lazy free 등 처리&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;1. Redis가 싱글 쓰레드라는 말의 의미&lt;/h2&gt;
&lt;p&gt;Redis가 싱글 쓰레드라고 할 때 핵심은 &lt;strong&gt;데이터를 읽고 쓰는 명령 실행 경로가 기본적으로 하나의 메인 쓰레드에서 처리된다&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 명령이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
GET user:1
INCR view:post:100
LPUSH queue:order 1001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령들은 Redis 내부의 메인 이벤트 루프에서 순차적으로 실행된다.&lt;/p&gt;
&lt;p&gt;즉, 여러 클라이언트가 동시에 요청을 보내더라도 Redis는 내부적으로 명령을 하나씩 꺼내 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client A -&amp;gt; SET user:1
Client B -&amp;gt; GET user:1
Client C -&amp;gt; INCR view:post:100

Redis Main Thread
1. SET user:1 처리
2. GET user:1 처리
3. INCR view:post:100 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조 덕분에 Redis는 명령 실행 중에 여러 쓰레드가 같은 자료구조를 동시에 수정하는 문제를 피할 수 있다.&lt;/p&gt;
&lt;p&gt;멀티 쓰레드 구조라면 다음과 같은 고민이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thread 1: user:1 수정
Thread 2: user:1 조회
Thread 3: user:1 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 동시성 제어를 위해 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 처리하기 때문에, 내부 자료구조 접근에 대해 복잡한 lock 경합을 크게 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;이게 Redis가 단순하면서도 빠른 이유 중 하나다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 그럼 Redis 프로세스에는 쓰레드가 하나만 있을까?&lt;/h2&gt;
&lt;p&gt;그건 아니다.&lt;/p&gt;
&lt;p&gt;Redis에는 메인 쓰레드 외에도 백그라운드 작업을 처리하기 위한 쓰레드들이 존재한다.&lt;/p&gt;
&lt;p&gt;책에서 본 것처럼 Redis는 표면적으로는 싱글 쓰레드처럼 설명되지만, 실제로는 메인 쓰레드 외에 백그라운드 쓰레드가 함께 동작한다.&lt;/p&gt;
&lt;p&gt;대표적으로 Redis의 background I/O, 즉 BIO 쓰레드는 다음과 같은 작업을 담당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
- 클라이언트 연결 처리
- 명령 파싱
- 명령 실행
- 응답 생성

Background I/O Threads
- BIO_CLOSE_FILE
- BIO_AOF_FSYNC
- BIO_LAZY_FREE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 백그라운드 쓰레드가 Redis의 핵심 데이터 명령을 병렬로 실행하는 것이 아니라는 점이다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;HSET&lt;/code&gt; 같은 명령을 여러 쓰레드가 동시에 나눠 실행하는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;백그라운드 쓰레드는 주로 메인 쓰레드가 직접 처리하면 오래 걸릴 수 있는 작업을 뒤로 넘겨서 처리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Redis의 3가지 백그라운드 쓰레드&lt;/h2&gt;
&lt;p&gt;Redis의 백그라운드 I/O 쓰레드는 역할별로 나누어 볼 수 있다.&lt;/p&gt;
&lt;p&gt;대표적으로 다음 3가지가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. BIO_CLOSE_FILE
   - 파일 닫기 작업 처리

2. BIO_AOF_FSYNC
   - AOF 파일 fsync 처리

3. BIO_LAZY_FREE
   - 큰 객체의 메모리 해제 작업 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 쓰레드는 Redis 명령을 대신 실행하는 것이 아니라, &lt;strong&gt;메인 쓰레드가 오래 붙잡고 있으면 위험한 보조 작업을 비동기적으로 처리한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3-1. BIO_CLOSE_FILE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_CLOSE_FILE&lt;/code&gt;은 이름 그대로 파일을 닫는 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;일반적으로 파일을 닫는 &lt;code&gt;close()&lt;/code&gt; 작업은 매우 빠를 것처럼 보인다. 하지만 운영체제나 파일 시스템 상황에 따라 파일을 닫는 과정이 예상보다 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 Redis가 AOF 파일을 교체하거나, 임시 파일을 사용한 뒤 닫아야 하는 상황이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: 파일 작업 완료
2. Main Thread: 파일 닫기 작업을 BIO_CLOSE_FILE 큐에 등록
3. BIO_CLOSE_FILE Thread: 실제 close 작업 수행
4. Main Thread: 클라이언트 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 메인 쓰레드가 직접 파일 닫기를 처리하다가 지연되면, Redis는 그동안 다른 클라이언트 명령을 처리하지 못한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;나쁜 흐름

Main Thread
1. SET user:1 처리
2. 파일 close 처리 시작
3. close 작업 지연
4. GET user:2 대기
5. INCR view:1 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 Redis는 파일 닫기처럼 가끔 지연될 수 있는 작업을 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;개선된 흐름

Main Thread
1. SET user:1 처리
2. close 작업을 백그라운드 큐에 등록
3. GET user:2 처리
4. INCR view:1 처리

BIO_CLOSE_FILE Thread
1. 실제 파일 close 수행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 파일 닫기가 Redis 데이터 명령 실행과 직접 관련된 작업은 아니라는 점이다. 따라서 백그라운드로 넘겨도 Redis의 명령 처리 일관성을 해치지 않는다.&lt;/p&gt;
&lt;h3&gt;3-2. BIO_AOF_FSYNC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 AOF(Append Only File)의 &lt;code&gt;fsync&lt;/code&gt; 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;Redis에서 AOF를 사용하면 쓰기 명령이 AOF 파일에 기록된다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 명령을 실행했다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 켜두면 Redis는 이 쓰기 명령들을 파일에 append 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendonly.aof
SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 파일에 썼다는 것과 디스크에 안전하게 반영됐다는 것은 다르다.&lt;/p&gt;
&lt;p&gt;운영체제는 성능을 위해 파일 쓰기를 메모리 버퍼에 잠시 보관할 수 있다. 이때 &lt;code&gt;fsync&lt;/code&gt;는 “버퍼에 있는 내용을 실제 디스크에 반영해라”라고 요청하는 작업이다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;code&gt;fsync&lt;/code&gt;가 디스크 I/O 작업이기 때문에 느려질 수 있다는 점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread가 fsync까지 직접 처리하는 경우

1. SET user:1 처리
2. AOF 파일에 기록
3. fsync 수행
4. 디스크 응답 대기
5. 다음 명령 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 흐름에서는 디스크가 느려지는 순간 Redis 전체 응답 시간이 흔들릴 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;appendfsync everysec&lt;/code&gt; 설정에서는 보통 fsync를 매 명령마다 하지 않고, 약 1초 단위로 백그라운드에서 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
1. SET user:1 처리
2. AOF 버퍼 또는 파일에 append
3. 클라이언트에 응답
4. 다음 명령 처리

BIO_AOF_FSYNC Thread
1. 주기적으로 AOF 파일 fsync 수행
2. 디스크 반영 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 장점은 메인 쓰레드가 디스크 fsync 때문에 오래 멈추는 상황을 줄일 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;다만 트레이드오프도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 매 쓰기마다 fsync
- 내구성 강함
- 성능 저하 가능성 큼

appendfsync everysec
- 보통 1초 단위 fsync
- 성능과 내구성의 균형
- 장애 시 최근 약 1초 데이터 유실 가능

appendfsync no
- fsync를 운영체제에 맡김
- 성능은 좋을 수 있음
- 내구성 보장은 약함&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, &lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 Redis의 명령 실행을 빠르게 유지하면서도 AOF 내구성을 확보하기 위한 백그라운드 작업이라고 볼 수 있다.&lt;/p&gt;
&lt;h3&gt;3-3. BIO_LAZY_FREE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.&lt;/p&gt;
&lt;p&gt;Redis에서 key를 삭제할 때 실제로는 두 가지 일이 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. keyspace에서 key 제거
2. 해당 value가 사용하던 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;작은 문자열 key라면 이 작업은 매우 빠르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL user:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 value가 매우 큰 List, Set, Sorted Set, Hash라면 이야기가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 &lt;code&gt;big:list&lt;/code&gt;에 수백만 개의 요소가 들어 있다면, key를 제거하는 것보다 내부 요소들의 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL big:list

Main Thread
1. keyspace에서 big:list 찾기
2. key 제거
3. list 내부 요소 메모리 해제
4. 해제가 끝날 때까지 다른 명령 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt;를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 먼저 keyspace에서 제거한다. 그러면 이후 Redis 명령에서는 해당 key에 접근할 수 없다.&lt;/p&gt;
&lt;p&gt;그리고 실제 메모리 해제 작업은 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt; 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UNLINK big:list

Main Thread
1. keyspace에서 big:list 제거
2. 메모리 해제 작업을 BIO_LAZY_FREE 큐에 등록
3. 바로 다음 명령 처리

BIO_LAZY_FREE Thread
1. big:list 내부 요소 메모리 해제
2. 사용하던 메모리 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조가 안전한 이유는 메인 쓰레드가 먼저 keyspace에서 key를 제거하기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread가 keyspace에서 key 제거
2. 이후 어떤 Redis 명령도 그 key에 접근할 수 없음
3. Background Thread가 실제 value 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 백그라운드 쓰레드가 Redis 자료구조를 마음대로 수정하는 것이 아니라, 이미 접근 불가능해진 객체의 메모리를 정리하는 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 &lt;code&gt;UNLINK&lt;/code&gt;뿐 아니라 설정에 따라 만료, eviction, &lt;code&gt;FLUSHDB ASYNC&lt;/code&gt;, &lt;code&gt;FLUSHALL ASYNC&lt;/code&gt; 같은 작업에서도 사용될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key
FLUSHDB ASYNC
FLUSHALL ASYNC&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;관련 설정으로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정들은 eviction, expire, 서버 내부 삭제, replica flush 같은 상황에서 메모리 해제를 가능한 한 백그라운드로 넘길지 결정한다.&lt;/p&gt;
&lt;p&gt;정리하면 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 Redis의 싱글 쓰레드 구조에서 가장 체감하기 쉬운 백그라운드 쓰레드다. 큰 key 삭제나 전체 flush 작업이 메인 쓰레드를 오래 막지 않도록 도와주기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 왜 백그라운드 쓰레드가 필요할까?&lt;/h2&gt;
&lt;p&gt;Redis는 메인 쓰레드가 클라이언트 요청을 순차적으로 처리한다.&lt;/p&gt;
&lt;p&gt;이 구조에서는 하나의 작업이 오래 걸리면 뒤에 있는 요청들이 모두 기다려야 한다.&lt;/p&gt;
&lt;p&gt;예를 들어 아주 큰 key를 삭제한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 &lt;code&gt;big:list&lt;/code&gt;가 매우 큰 리스트라면, 삭제 과정에서 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;Redis 메인 쓰레드가 이 작업을 끝까지 직접 처리하면 그동안 다른 요청 처리가 지연될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. DEL big:list 처리 시작
2. 큰 메모리 해제 작업 수행
3. 작업이 끝날 때까지 다른 요청 대기
4. 이후 GET, SET 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt; 같은 명령을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 논리적으로 제거한 뒤, 실제 메모리 해제 작업은 백그라운드에서 처리할 수 있게 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: key를 keyspace에서 제거
2. BIO_LAZY_FREE Thread: 실제 메모리 해제
3. Main Thread: 다음 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis가 백그라운드 쓰레드를 사용하는 이유는 핵심 명령 실행을 병렬화하기 위해서라기보다, &lt;strong&gt;메인 이벤트 루프가 오래 막히지 않도록 느린 작업을 분리하기 위해서&lt;/strong&gt;라고 보는 편이 맞다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Redis 6부터는 I/O Thread도 있다&lt;/h2&gt;
&lt;p&gt;Redis 6부터는 I/O Thread 기능이 추가되었다.&lt;/p&gt;
&lt;p&gt;여기서 다시 헷갈릴 수 있다.&lt;/p&gt;
&lt;p&gt;“Redis 6부터 멀티 쓰레드라면 이제 명령도 병렬로 실행되는 건가?”&lt;/p&gt;
&lt;p&gt;그렇지는 않다.&lt;/p&gt;
&lt;p&gt;Redis 6의 I/O Thread는 주로 네트워크 I/O를 병렬화하기 위한 기능이다.&lt;/p&gt;
&lt;p&gt;Redis 요청 처리를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 클라이언트 요청 읽기
2. 명령 파싱
3. 명령 실행
4. 응답 쓰기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존에는 이 흐름 대부분을 메인 쓰레드가 처리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
Read -&amp;gt; Parse -&amp;gt; Execute -&amp;gt; Write&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 6 이후에는 설정에 따라 일부 네트워크 읽기/쓰기 작업을 I/O Thread가 도와줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I/O Thread
- socket read
- socket write
- command parsing 일부

Main Thread
- command execute
- Redis data structure access
- reply 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis 6 이후에도 핵심은 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;네트워크 I/O 일부는 멀티 쓰레드 가능
하지만 Redis 자료구조를 변경하는 명령 실행은 메인 쓰레드 중심&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 Redis의 I/O Thread와 앞에서 설명한 BIO 쓰레드는 역할이 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BIO Thread
- 파일 닫기
- AOF fsync
- lazy free
- 메인 쓰레드가 오래 막힐 수 있는 내부 작업 처리

I/O Thread
- 클라이언트 socket read/write 보조
- 네트워크 I/O 병목 완화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 둘 다 Redis 프로세스 내부의 추가 쓰레드지만 목적이 다르다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO Thread&lt;/code&gt;는 Redis 내부의 느린 작업을 백그라운드로 넘기기 위한 쓰레드이고, &lt;code&gt;I/O Thread&lt;/code&gt;는 클라이언트 네트워크 I/O 처리량을 높이기 위한 쓰레드다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Redis가 싱글 쓰레드인데도 빠른 이유&lt;/h2&gt;
&lt;p&gt;Redis가 빠른 이유를 단순히 “메모리를 사용하기 때문”이라고만 보면 부족하다.&lt;/p&gt;
&lt;p&gt;물론 Redis는 대부분의 데이터를 메모리에 두기 때문에 디스크 기반 DB보다 빠른 접근이 가능하다.&lt;/p&gt;
&lt;p&gt;하지만 그 외에도 몇 가지 이유가 있다.&lt;/p&gt;
&lt;h3&gt;1) 명령 실행이 단순하다&lt;/h3&gt;
&lt;p&gt;Redis 명령은 대부분 짧고 빠르게 끝난다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;GET user:1
SET token:abc &amp;quot;...&amp;quot;
INCR view:post:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 명령은 일반적인 RDBMS 쿼리처럼 복잡한 실행계획을 세우거나, 여러 테이블을 조인하거나, 디스크 블록을 많이 읽는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;대부분 메모리의 자료구조에 직접 접근한다.&lt;/p&gt;
&lt;h3&gt;2) I/O Multiplexing을 사용한다&lt;/h3&gt;
&lt;p&gt;Redis는 하나의 쓰레드로도 여러 클라이언트 연결을 다룰 수 있다.&lt;/p&gt;
&lt;p&gt;이때 사용하는 방식이 I/O Multiplexing이다.&lt;/p&gt;
&lt;p&gt;쉽게 말하면, 클라이언트마다 쓰레드를 하나씩 만드는 것이 아니라, 하나의 이벤트 루프가 여러 소켓을 감시하다가 준비된 요청만 처리하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client 1
Client 2
Client 3
Client 4
   ↓
Event Loop
   ↓
Ready 된 요청만 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조는 Node.js의 이벤트 루프와도 비슷하게 이해할 수 있다.&lt;/p&gt;
&lt;h3&gt;3) Lock 경합이 적다&lt;/h3&gt;
&lt;p&gt;멀티 쓰레드로 명령을 동시에 실행하면 공유 데이터에 대한 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 순차적으로 처리하기 때문에 lock 경합 비용이 작다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;멀티 쓰레드 방식
- 동시에 실행 가능
- lock 필요
- context switching 비용 발생
- 동시성 버그 가능성 증가

Redis 방식
- 명령 실행은 순차 처리
- lock 경합 감소
- 구현 단순
- 예측 가능한 실행 흐름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis는 이 단순한 구조를 선택한 대신, 오래 걸리는 작업을 피하거나 백그라운드로 넘기는 방식으로 성능을 유지한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 싱글 쓰레드 구조에서 조심해야 할 명령&lt;/h2&gt;
&lt;p&gt;Redis가 빠르다고 해서 모든 명령이 항상 안전한 것은 아니다.&lt;/p&gt;
&lt;p&gt;싱글 쓰레드 구조에서는 오래 걸리는 명령 하나가 전체 요청 처리에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;대표적인 예가 &lt;code&gt;KEYS&lt;/code&gt;다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;KEYS *&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령은 전체 keyspace를 순회한다.&lt;/p&gt;
&lt;p&gt;운영 환경에서 key가 많다면 메인 쓰레드가 오랫동안 이 작업을 수행하게 되고, 그동안 다른 요청이 밀릴 수 있다.&lt;/p&gt;
&lt;p&gt;개선된 방식은 &lt;code&gt;SCAN&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SCAN 0 MATCH user:* COUNT 100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt;은 전체를 한 번에 훑는 것이 아니라 cursor 기반으로 조금씩 순회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;KEYS
- 전체 key를 한 번에 조회
- 메인 쓰레드가 오래 점유될 수 있음
- 운영 환경에서 위험

SCAN
- cursor 기반으로 나눠 조회
- 한 번에 처리하는 양을 조절 가능
- 운영 환경에서 상대적으로 안전&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 key 삭제도 마찬가지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:key&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 객체를 삭제할 때는 다음처럼 &lt;code&gt;UNLINK&lt;/code&gt;를 고려할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL
- key 삭제와 메모리 해제를 동기적으로 처리할 수 있음
- 큰 key에서는 지연 발생 가능

UNLINK
- keyspace에서는 빠르게 제거
- 실제 메모리 해제는 BIO_LAZY_FREE가 백그라운드 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 사용하는 환경에서는 &lt;code&gt;appendfsync&lt;/code&gt; 설정도 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;appendonly yes
appendfsync everysec&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보통은 &lt;code&gt;everysec&lt;/code&gt;가 성능과 내구성의 균형점으로 많이 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 쓰기마다 디스크 동기화
- 안전하지만 느려질 수 있음

appendfsync everysec
- 약 1초 단위로 fsync
- BIO_AOF_FSYNC가 백그라운드에서 처리
- 일반적인 운영 환경에서 많이 사용

appendfsync no
- 운영체제 정책에 맡김
- 빠를 수 있지만 장애 시 유실 범위가 커질 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;8. Redis를 “싱글 쓰레드”라고만 말하면 부족한 이유&lt;/h2&gt;
&lt;p&gt;Redis를 이해할 때는 관점을 나눠야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 명령 실행 관점
   - 대부분 싱글 쓰레드
   - Redis 자료구조 접근은 메인 쓰레드 중심

2. Redis 프로세스 관점
   - 메인 쓰레드 외 백그라운드 쓰레드 존재
   - BIO_CLOSE_FILE, BIO_AOF_FSYNC, BIO_LAZY_FREE 등 처리

3. Redis 6 이후 네트워크 I/O 관점
   - I/O Thread 사용 가능
   - socket read/write 일부를 병렬 처리 가능

4. Persistence 관점
   - AOF fsync는 백그라운드 쓰레드가 도울 수 있음
   - RDB snapshot, AOF rewrite는 별도 프로세스가 관여할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 정확히 표현하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Redis는 클라이언트 명령 실행과 데이터 접근은 주로 싱글 쓰레드로 처리하지만, 백그라운드 I/O 작업과 네트워크 I/O 최적화를 위해 여러 쓰레드 또는 별도 프로세스를 사용할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;9. 실제 운영에서는 어떻게 봐야 할까?&lt;/h2&gt;
&lt;p&gt;Redis를 운영할 때는 “싱글 쓰레드니까 CPU 코어 하나만 보면 된다”라고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;물론 명령 실행 자체는 메인 쓰레드 병목이 될 수 있다.&lt;br&gt;그래서 Redis는 보통 하나의 인스턴스가 하나의 CPU 코어를 강하게 사용하는 형태로 나타날 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 다음 요소들도 함께 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;- 메인 쓰레드 CPU 사용률
- 네트워크 I/O 병목
- 큰 key 삭제 여부
- AOF fsync 지연
- RDB snapshot 또는 AOF rewrite 시점
- slowlog
- latency monitor
- lazy free pending object 증가 여부&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 Redis가 느려졌을 때는 단순히 CPU만 볼 게 아니라 다음 명령들을 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SLOWLOG GET 10&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO stats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO commandstats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO persistence&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO memory&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;INFO memory&lt;/code&gt;에서는 lazy free와 관련된 지표를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lazyfree_pending_objects&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 값이 계속 증가한다면 백그라운드에서 해제해야 할 객체가 많이 쌓이고 있다는 의미로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;또 AOF를 사용하는 경우에는 &lt;code&gt;INFO persistence&lt;/code&gt;를 통해 AOF rewrite, fsync, persistence 관련 상태를 함께 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 마무리&lt;/h2&gt;
&lt;p&gt;Redis는 흔히 싱글 쓰레드라고 설명된다.&lt;/p&gt;
&lt;p&gt;이 말은 틀린 말은 아니지만, 정확히는 &lt;strong&gt;Redis의 핵심 명령 실행 경로가 싱글 쓰레드에 가깝다&lt;/strong&gt;는 의미로 이해해야 한다.&lt;/p&gt;
&lt;p&gt;Redis 프로세스 전체를 보면 메인 쓰레드 외에도 백그라운드 쓰레드가 존재하고, Redis 6 이후에는 네트워크 I/O를 위한 I/O Thread도 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Redis는 완전한 의미의 단일 쓰레드 프로그램은 아니다.

하지만 Redis의 핵심 데이터 명령 실행은 여전히 메인 쓰레드 중심으로 동작한다.

BIO_CLOSE_FILE은 파일 닫기 작업을 백그라운드에서 처리한다.

BIO_AOF_FSYNC는 AOF fsync 작업을 백그라운드에서 처리한다.

BIO_LAZY_FREE는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.

Redis 6 이후의 I/O Thread도 명령 실행을 병렬화하는 것이 아니라 네트워크 I/O 병목을 줄이기 위한 기능이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 Redis의 싱글 쓰레드 모델은 단순한 제약이 아니라, 빠른 메모리 접근과 이벤트 루프, lock 없는 명령 실행을 조합한 설계 선택이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;Redis를 사용할 때 중요한 것은 “싱글 쓰레드냐, 멀티 쓰레드냐”를 외우는 것이 아니라, &lt;strong&gt;어떤 작업이 메인 쓰레드를 막고 어떤 작업이 백그라운드로 분리될 수 있는지 이해하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Redis 공식 문서 - Latency optimization&lt;/li&gt;
&lt;li&gt;Redis 공식 문서 - INFO command&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - The little-known feature of Redis 4.0 that will speed up your applications&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - Redis 8.0-M03 is out. Even more performance &amp;amp; new features&lt;/li&gt;
&lt;li&gt;Redis GitHub source - &lt;code&gt;src/bio.c&lt;/code&gt;, &lt;code&gt;src/bio.h&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/259</guid>
      <comments>https://soobarkbar.tistory.com/259#entry259comment</comments>
      <pubDate>Sun, 7 Jun 2026 17:44:48 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 풀스캔(Index Full Scan) 훑어보기</title>
      <link>https://soobarkbar.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 보다 보면 &lt;code&gt;range&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt; 같은 접근 방식과 함께&lt;br /&gt;DBMS에 따라 &lt;b&gt;인덱스 풀스캔&lt;/b&gt;에 가까운 동작이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 보면 비효율적으로 느껴질 수 있지만, 실제로는 상황에 따라 꽤 합리적인 선택이 된다.&lt;br /&gt;중요한 건 &lt;b&gt;&amp;ldquo;인덱스를 일부만 읽는 스캔이 아니라, 인덱스 전체를 읽는 스캔&amp;rdquo;&lt;/b&gt; 이라는 점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인덱스 풀스캔이 무엇이며, 어떻게 동작하는가&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;br /&gt;예를 들어 &lt;code&gt;where id = 10&lt;/code&gt; 같은 조건에서는 인덱스의 일부만 읽으면 된다.&lt;br /&gt;그런데 어떤 쿼리는 조건 검색보다도 &lt;b&gt;정렬된 순서&lt;/b&gt;나 &lt;b&gt;인덱스에 담긴 컬럼 자체&lt;/b&gt;가 더 중요하다.&lt;br /&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 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;인덱스는 보통 테이블보다 &lt;b&gt;폭이 더 좁다&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 건을 저장하는 데이터 크기가 더 작아서 같은 양을 읽을 때 필요한 블록 수가 더 적을 수 있다. 용량을 많이 차지하는 데이터 타입 (varchar(1000), blob, ...) 이 있다면 하나의 블록에 들어갈 수 있는 로우 수가 적어지기 때문에 테이블의 폭이 더 넓을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B-Tree 인덱스는 키 순서대로 정렬되어 있다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스 안에 있으면 테이블까지 안 가도 되는 경우가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;전체를 읽더라도 테이블보다 인덱스가 비용이 더 싸다&amp;rdquo;라고 판단되면 인덱스 풀스캔이 나올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 동작 방식&lt;/h3&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;필요한 컬럼이 인덱스에 다 있으면 결과를 바로 반환한다&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE employees (
    emp_id INT,
    dept_id INT,
    emp_name VARCHAR(100),
    hire_date DATE,
    salary INT
);

CREATE INDEX idx_emp_hire_date ON employees(hire_date);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 hire_date만 필요하고, 정렬도 hire_date 기준이다.&lt;br /&gt;이 경우 테이블을 전부 읽고 정렬하는 것보다, idx_emp_hire_date 인덱스를 처음부터 끝까지 읽는 편이 더 유리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인덱스 풀스캔이 유리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 &amp;ldquo;풀스캔&amp;rdquo;이라는 이름 때문에 무조건 나쁜 방식처럼 보이지만, 실제로는 꽤 유용한 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 필요한 컬럼이 인덱스에 다 들어 있는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 가장 유리하다.&lt;br /&gt;인덱스만 읽고 결과를 만들 수 있으니 테이블 접근을 줄일 수 있다. MySQL에서는 이런 경우 Using index가 붙는 covering index 형태가 될 수 있고, Oracle에서도 인덱스만으로 결과를 만들 수 있을 때 빠른 경로를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary);

SELECT dept_id, salary
FROM employees
ORDER BY dept_id, salary;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 컬럼도 인덱스에 있고, 정렬 순서도 인덱스 순서와 맞는다.&lt;br /&gt;이런 경우 인덱스를 전체로 읽는 방식이 꽤 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 결과를 이미 정렬된 상태로 가져오고 싶은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 키 순서대로 저장된다.&lt;br /&gt;그래서 ORDER BY가 인덱스 순서와 맞아떨어지면 별도의 정렬 작업을 줄일 수 있다. MySQL은 인덱스를 이용해 정렬을 피할 수 있고, Oracle의 일반적인 index full scan도 인덱스 키 순서를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전체를 읽고 sort 하는 것보다, hire_date 인덱스를 순서대로 읽는 편이 더 나을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 테이블보다 인덱스가 훨씬 가벼운 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 컬럼이 많고 행 크기가 큰 반면, 인덱스는 상대적으로 얇다.&lt;br /&gt;그래서 &amp;ldquo;어차피 많이 읽어야 한다면 차라리 인덱스를 읽자&amp;rdquo;라는 판단이 가능하다. MySQL 문서도 covering index의 경우 인덱스만 스캔하는 것이 보통 테이블 전체를 읽는 것보다 빠를 수 있다고 설명한다.&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;employees 테이블에 큰 TEXT, JSON, BLOB 컬럼이 많다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT dept_id
FROM employees
ORDER BY dept_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 dept_id 인덱스만 훑는 편이 테이블 전체를 읽는 것보다 부담이 적을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인덱스 풀스캔이 불리한 경우&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 data-ke-size=&quot;size23&quot;&gt;1) 대부분 행에 대해 다시 테이블 접근이 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에는 키 값만 있고 실제 필요한 컬럼은 테이블에 있는 경우가 많다.&lt;br /&gt;이때 인덱스를 전체로 읽은 뒤, 각 행마다 다시 테이블을 찾아가면 랜덤 I/O가 많이 생길 수 있다.&lt;br /&gt;이 경우는 차라리 테이블 풀스캔이 더 나을 수 있다. Oracle도 다른 접근 경로보다 비용이 높으면 full table scan을 선택한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hire_date 인덱스는 정렬에는 도움이 되지만, SELECT * 이므로 결국 대부분 행을 다시 테이블에서 읽어야 한다.&lt;br /&gt;데이터 양이 많으면 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 조건으로 일부만 읽어도 되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상이 일부 구간에 불과하다면 인덱스 전체를 읽는 것보다 레인지 스캔이 더 낫다.&lt;br /&gt;MySQL 문서도 range access는 인덱스 값의 일부 구간만 읽는 방식이라고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT emp_id, hire_date
FROM employees
WHERE hire_date BETWEEN '2025-01-01' AND '2025-01-31';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 전체 인덱스를 다 볼 필요가 없다.&lt;br /&gt;조건에 맞는 구간만 읽는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 인덱스 정렬 순서가 쿼리와 맞지 않는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 전체로 읽더라도 원하는 정렬이나 필터링 조건과 맞지 않으면 이점이 줄어든다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_id ON employees(dept_id);

SELECT salary
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인덱스는 dept_id 기준인데, 쿼리는 hire_date 기준으로 정렬한다.&lt;br /&gt;이런 경우 idx_emp_dept_id를 풀스캔해도 정렬 이점을 얻기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL과 Oracle에서 인덱스 풀스캔이 어떻게 다르게 동작하는지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &amp;ldquo;인덱스를 많이 읽는다&amp;rdquo;는 점은 비슷하지만, 실행 계획에서의 의미와 세부 동작은 다르게 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 인덱스 전체를 읽는 방식도 두 가지로 나눠서 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FULL SCAN&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;ORDER BY를 만족시키는 데 유리하다&lt;/li&gt;
&lt;li&gt;보통 단일 블록 I/O 성격으로 이해한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FAST FULL SCAN&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;멀티블록 I/O, 병렬 처리 활용이 가능하다&lt;/li&gt;
&lt;li&gt;물리적으로 디스크에 저장된 순서대로 블록을 읽기 때문에 결과가 인덱스 순서로 나오지 않을 수 있다&lt;/li&gt;
&lt;li&gt;인덱스에 필요한 컬럼이 모두 있을 때 full table scan의 대안이 될 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) MySQL은 보통 type=index와 Using index를 같이 해석해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 실행 계획에서는 Oracle처럼 INDEX FULL SCAN, INDEX FAST FULL SCAN이라는 이름으로 세분화해서 보여주지 않는다.&lt;br /&gt;대신 EXPLAIN 결과에서 type=index가 나오면 인덱스를 처음부터 끝까지 읽는 스캔에 가깝게 해석하는 경우가 많다.&lt;br /&gt;그리고 Extra에 Using index가 있으면 인덱스만으로 결과를 처리하는 covering index 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Oracle은 &amp;ldquo;정렬 보장 여부&amp;rdquo;까지 구분해서 보고, MySQL은 실행 계획 컬럼 조합으로 해석하는 편이다&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;표현 방식&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN, INDEX FAST FULL SCAN 등으로 구분&lt;/td&gt;
&lt;td&gt;보통 type=index, Extra=Using index 등 조합으로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 순서 활용&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN은 키 순서 활용 가능&lt;/td&gt;
&lt;td&gt;인덱스 순서를 활용할 수 있지만 Oracle처럼 명시적으로 scan 이름이 나뉘진 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스만 읽는 빠른 전체 스캔&lt;/td&gt;
&lt;td&gt;INDEX FAST FULL SCAN 존재&lt;/td&gt;
&lt;td&gt;covering index scan 형태로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 단순히 &amp;ldquo;인덱스를 썼으니 좋은 것&amp;rdquo;, 또는 &amp;ldquo;풀스캔이니 무조건 나쁜 것&amp;rdquo;으로 보면 안 된다.&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;정렬 이점이나 covering index가 있으면 유리할 수 있다&lt;/li&gt;
&lt;li&gt;반대로 테이블 재접근이 많아지면 비효율적일 수 있다&lt;/li&gt;
&lt;li&gt;Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분해서 봐야 하고,&lt;br /&gt;MySQL은 type=index, Using index 등을 조합해서 해석해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 볼 때는 &amp;ldquo;인덱스를 탔는가&amp;rdquo;만 보지 말고, 인덱스를 어떻게 탔는가까지 같이 봐야 한다.&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/258</guid>
      <comments>https://soobarkbar.tistory.com/258#entry258comment</comments>
      <pubDate>Sat, 18 Apr 2026 16:57:51 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] MacOS에서 카프카 매니저 설치</title>
      <link>https://soobarkbar.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. github에서 tar.gz 파일 다운로드 (&lt;a href=&quot;https://github.com/yahoo/CMAK&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yahoo/CMAK&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZKBf%2FbtsGrW9BOEb%2FzHJGyiMHUMJgQNSevDX750%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;2312&quot; height=&quot;1114&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&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;size18&quot;&gt;2. 압축 해제 후 설치한 폴더 내에서 ./sbt clean dist 명령어 실행&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Cannot use JVMCI compiler: No JVMCI compiler found&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;명령어 실행 시 위 에러가 발생한 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/927&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. target/universal 경로에 생성된 .zip 파일을 원하는 위치에 압축 해제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. conf/application.conf 파일 수정 (로컬에 주키퍼를 띄웠기 때문에 아래와 같이 수정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnqPxL%2FbtsGpKXqiXR%2FuNZJ1NEpLhVKhSsL3cRH70%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;600&quot; height=&quot;113&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5.  ./bin/cmak 명령어 실행 후, localhost:9000 접속&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAkO6m%2FbtsGprqkxz5%2F5wl7mrfkvQC1Mwcp7hH4Bk%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;3340&quot; height=&quot;604&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&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;!&amp;nbsp;@7jn3njc8f&amp;nbsp;-&amp;nbsp;Internal&amp;nbsp;server&amp;nbsp;error,&amp;nbsp;for&amp;nbsp;(GET)&amp;nbsp;[/assets/dataTables/javascripts/dataTables.bootstrap4.js]&lt;/li&gt;
&lt;li&gt;카프카 매니저 접속 시에 JS, CSS를 불러오지 못하는 위 에러가 발생하는 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/844&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/256</guid>
      <comments>https://soobarkbar.tistory.com/256#entry256comment</comments>
      <pubDate>Sat, 6 Apr 2024 14:54:32 +0900</pubDate>
    </item>
    <item>
      <title>기수 정렬 (Radix Sort)</title>
      <link>https://soobarkbar.tistory.com/156</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기수 정렬 (Radix Sort) ?&lt;/h2&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: #ee2323;&quot;&gt;&lt;b&gt;각 자리 위치를 하나씩 증가시키면서&lt;/b&gt;&lt;/span&gt; &lt;b&gt;숫자들을 정렬&lt;/b&gt;하는 방법이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정 정렬&lt;/b&gt;에 속하며, &lt;a href=&quot;https://soobarkbar.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카운팅 정렬&lt;/a&gt;과 마찬가지로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;값의 비교연산 없이&lt;/span&gt; 정렬&lt;/b&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;추가적인 메모리가 필요&lt;/b&gt;&lt;/span&gt;하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;동일한 길이를 가진&lt;/span&gt; 숫자나 문자열&lt;/b&gt;이여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정렬하는 숫자 자릿수 \(k\) 에 따라 \(O(kn)\) 의 시간 복잡도를 가진다.&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;1의 자리 숫자를 비교해서 오름차순 또는 내림차순으로 정렬한 뒤, 큐 버킷에 삽입한 뒤 순서대로 뽑아 정렬한다.&lt;/li&gt;
&lt;li&gt;그 다음, 10의 자리 숫자를 비교해서 동일하게 수행한다.&lt;/li&gt;
&lt;li&gt;가장 큰 숫자의 자릿 수만큼 반복해서 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF5fiB%2FbtqCgSllX0Z%2FkECRdI1FQquPfMjBbkoZEK%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;1057&quot; height=&quot;419&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code&lt;/h3&gt;
&lt;pre id=&quot;code_1696321370732&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void radixSort(int[] arr, int maxSize) {
    int jarisu = 1, size = 0;
    int len = arr.length;
    int[] output = new int[len];

    while (size &amp;lt; maxSize) {
        int[] bucket = new int[10];

        for (int i = 0; i &amp;lt; len; ++i) {
            ++bucket[arr[i] / jarisu % 10];
        }

        for (int i = 1; i &amp;lt; 10; ++i) {
            bucket[i] += bucket[i - 1];
        }

        for (int i = len - 1; i &amp;gt;= 0; --i) {
            int idx = arr[i] / jarisu % 10;

            output[bucket[idx] - 1] = arr[i];
            --bucket[idx];
        }

        for (int i = 0; i &amp;lt; len; ++i) {
            arr[i] = output[i];
        }

        jarisu *= 10;
        ++size;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;
&lt;table class=&quot;colorscripter-code-table&quot; style=&quot;margin: 0; padding: 0; border: none; background-color: #272727; border-radius: 4px;&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px; border-right: 2px solid #4f4f4f;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; word-break: normal; text-align: right; color: #aaa; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;1&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;2&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;3&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;4&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 6px 0; text-align: left;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;1&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[82,&amp;nbsp;43,&amp;nbsp;3,&amp;nbsp;15,&amp;nbsp;35,&amp;nbsp;27,&amp;nbsp;7,&amp;nbsp;38,&amp;nbsp;18,&amp;nbsp;9]&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;2&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[3,&amp;nbsp;7,&amp;nbsp;9,&amp;nbsp;15,&amp;nbsp;18,&amp;nbsp;27,&amp;nbsp;35,&amp;nbsp;38,&amp;nbsp;43,&amp;nbsp;82]&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;vertical-align: bottom; padding: 0 2px 4px 0;&quot;&gt;&lt;a style=&quot;text-decoration: none; color: white;&quot; href=&quot;http://colorscripter.com/info#e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-size: 9px; word-break: normal; background-color: #4f4f4f; color: white; border-radius: 10px; padding: 1px;&quot;&gt;cs&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://lktprogrammer.tistory.com/48&quot;&gt;https://lktprogrammer.tistory.com/48&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&quot;&gt;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/Sort</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/156</guid>
      <comments>https://soobarkbar.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 3 Oct 2023 17:23:20 +0900</pubDate>
    </item>
    <item>
      <title>Forward / Reverse Proxy</title>
      <link>https://soobarkbar.tistory.com/248</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Proxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;프록시&amp;nbsp;서버를&amp;nbsp;통해서&amp;nbsp;다른&amp;nbsp;네트워크&amp;nbsp;서비스에&amp;nbsp;간접적으로&amp;nbsp;접속할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;해주는&amp;nbsp;컴퓨터&amp;nbsp;시스템&amp;nbsp;또는&amp;nbsp;응용&amp;nbsp;프로그램&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 (Proxy) : 서버와 클라이언트 사이에서 대리로 통신을 수행하는 것&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프록시 서버 (Proxy Server) : 중계 기능을 수행하는 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;1. Forward Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;접근하는&amp;nbsp;것이&amp;nbsp;아니라&amp;nbsp;프록시&amp;nbsp;서버가&amp;nbsp;요청을&amp;nbsp;받고&amp;nbsp;인터넷에&amp;nbsp;연결하여&amp;nbsp;결과를&amp;nbsp;클라이언트에&amp;nbsp;전달&amp;nbsp;(Forward)&amp;nbsp;해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;보안 : 프록시 서버에서 In / Out Bound 패킷에 대한 보안 정책 (Content Filtering 등) 을 적용할 수 있다.&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;성능 : 프록시 서버 내부에 캐시를 유지하며 한 번 통신한 외부 서버의 이미지, 파일 등을 저장할 수 있다. 캐시에 데이터가 있으면 프록시 서버가 데이터를 바로 제공할 수 있어 빠른 통신을 지원한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkfkRx%2FbtrA0XzX0Or%2F3MGQUCeH1LIiZaJ2G4KrZ0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;283&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;2. Reverse Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;데이터를&amp;nbsp;요청하면&amp;nbsp;리버스&amp;nbsp;프록시가&amp;nbsp;이&amp;nbsp;요청을&amp;nbsp;받아&amp;nbsp;내부&amp;nbsp;서버에서&amp;nbsp;데이터를&amp;nbsp;받은&amp;nbsp;후&amp;nbsp;클라이언트에&amp;nbsp;전달한다.&lt;br /&gt;&lt;br /&gt;클라이언트는&amp;nbsp;내부&amp;nbsp;서버에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;알&amp;nbsp;필요&amp;nbsp;없이&amp;nbsp;리버스&amp;nbsp;프록싱만&amp;nbsp;요청하면&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;내부&amp;nbsp;서버&amp;nbsp;(WAS)&amp;nbsp;에&amp;nbsp;직접&amp;nbsp;접근할&amp;nbsp;경우,&amp;nbsp;DB에&amp;nbsp;접근이&amp;nbsp;가능하기&amp;nbsp;때문에&amp;nbsp;중간에&amp;nbsp;리버스&amp;nbsp;프록시를&amp;nbsp;두고&amp;nbsp;클라이언트와&amp;nbsp;내부&amp;nbsp;서버&amp;nbsp;사이의&amp;nbsp;통신을&amp;nbsp;담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&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;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZDP0i%2FbtrA33Gzlfe%2F79jDfwoNKKzYWL7qkR63Hk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;287&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;차이점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 요청하는 End Point&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;실제 서버 도메인&lt;/b&gt;&lt;br /&gt;Reverse&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;프록시&amp;nbsp;서버&amp;nbsp;도메인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;감춰지는 대상&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward Proxy : &lt;b&gt;클라이언트 &lt;/b&gt;(요청 받는 서버는 포워드 프록시 서버를 통해서 요청을 받기 때문에 클라이언트의 정보를 알 수 없다.)&lt;br /&gt;Reverse Proxy : &lt;b&gt;서버&amp;nbsp;&lt;/b&gt;(클라이언트는 리버스 프록시 서버에게 요청하기 때문에 실제 서버의 정보를 알 수 없다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://bcp0109.tistory.com/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bcp0109.tistory.com/194&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/248</guid>
      <comments>https://soobarkbar.tistory.com/248#entry248comment</comments>
      <pubDate>Mon, 2 May 2022 12:19:13 +0900</pubDate>
    </item>
    <item>
      <title>count(*) / count(1) / count(컬럼) 차이</title>
      <link>https://soobarkbar.tistory.com/246</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;count(*) VS count(1)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; data-alt=&quot;출처 : mysql 홈페이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yl5XM/btrztXQDJVV/28BViKfccMQ62OF9xbvlYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYl5XM%2FbtrztXQDJVV%2F28BViKfccMQ62OF9xbvlYK%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;1672&quot; height=&quot;66&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : mysql 홈페이지&lt;/figcaption&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;차이가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;count(*) VS count(컬럼)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;count(*) : null을 포함하여 모든 Row 수를 카운트한다.&lt;/li&gt;
&lt;li&gt;count(컬럼명) : null을 제외한 Row 수를 카운트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_count&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://asktom.oracle.com/pls/apex/f?p=100:11:0::NO::P11_QUESTION_ID:1156159920245&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://asktom.oracle.com/pls/apex/f?p=100:11:0::NO::P11_QUESTION_ID:1156159920245&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/246</guid>
      <comments>https://soobarkbar.tistory.com/246#entry246comment</comments>
      <pubDate>Fri, 15 Apr 2022 17:59:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring MVC (Model, View, Controller)</title>
      <link>https://soobarkbar.tistory.com/244</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model / View / Controller로 역할을 나누는 패턴이다.&lt;/li&gt;
&lt;li&gt;MVC 패턴의 목적은 Business Logic과 Presentation Logic을 분리하는 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Business Logic : 어떤 특정한 값을 얻기 위해 데이터 처리를 수행하는 응용프로그램의 일부 (즉, 원하는 값을 얻기 위해 백엔드에서 일어나는 각종 처리)&lt;/li&gt;
&lt;li&gt;Presentation Logic : 화면상의 디자인 구성을 위한 처리 (즉, 화면을 보여주기 위한 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;역할 별로 코드를 분리하여 하나의 파일에 코드가 모이는 것을 방지하기 때문에, 가독성과 코드 재사용이 증가한다.&lt;/li&gt;
&lt;li&gt;단점으로 View와 Model 사이의 높은 의존성 때문에 애플리케이션이 커질수록 복잡해지고 유지보수가 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RO5QC/btrzraH9Fvs/KqAFxev7ivroOcuH7OG4vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRO5QC%2FbtrzraH9Fvs%2FKqAFxev7ivroOcuH7OG4vK%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;320&quot; height=&quot;350&quot; data-origin-width=&quot;320&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Model&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller에서 View로 전달되는 데이터 객체이다.&lt;/li&gt;
&lt;li&gt;Key-Value 형태를 가지며, 하나의 (Key-Value) 객체를 Model Attribute라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjSSQr/btrzuznobGr/Kuk3d79NjzBajWFiNvOKF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjSSQr%2FbtrzuznobGr%2FKuk3d79NjzBajWFiNvOKF1%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;200&quot; height=&quot;119&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Controller&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;View와 Model 사이의 인터페이스 역할을 수행한다.&lt;/li&gt;
&lt;li&gt;Request에 따라 적절한 결과를 Model에 담아 View에 전달한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, View Name과 View에 출력할 Model을 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;View&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model 데이터의 렌더링을 담당하며, HTML Output을 생성한다.&lt;/li&gt;
&lt;li&gt;여러가지 템플릿 엔진이 존재한다. (ex. JSP, Thymleaf, Freemarker, ...)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring MVC의 흐름&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8Msx/btrzoN1JQ6B/YmQKp1nnXmOiHlRb4znswK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8Msx%2FbtrzoN1JQ6B%2FYmQKp1nnXmOiHlRb4znswK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;588&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Client 요청 (Request) 이 들어오면 Filter를 거치고, Dispatcher Servlet으로 온다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Dispatcher Servlet은 받은 요청을 Handler Mapping에게 전달하고, Handler Mapping은 요청 URL을 분석하여 적절한 Controller 정보를 찾아 Dispatcher Servlet에게 반환한다.&lt;/li&gt;
&lt;li&gt;적절한 Controller 정보를 받은 Dispatcher Servlet은 Handler Adapter를 호출하고, 전달 받은 Controller 중 요청한 URL에 맞는 적절한 Method를 찾아준다.&lt;/li&gt;
&lt;li&gt;호출된 Controller는 Business Logic을 처리하고 나온 결과를 View에 전달할 수 있도록 Model 객체에 저장한다.&lt;/li&gt;
&lt;li&gt;그 후, 호출된 Controller는 Dispatcher Servlet에게 View Name을 반환한다.&lt;/li&gt;
&lt;li&gt;Dispatcher Servlet은 View Resolver를 호출하여 View Name을 전달하고, View Resolver는 View Name을 바탕으로 적절한 View 객체를 결정한다.&lt;/li&gt;
&lt;li&gt;View 객체는 적절한 View (ex. JSP, Thymeleaf) 를 호출하고, View는 화면 표시에 필요한 데이터를 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Model 객체에서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;가져와 화면 표시를 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;렌더링된 View 화면을 Client에게 Response 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구성 요소&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Dispatcher Servlet&lt;/h4&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;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Spring Framework가 제공하는 Servlet 클래스로, Front Controller를 담당하고 있어&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;모든 HTTP 요청을 받는다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Handler Mapping&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request URL에 해당하는 Controller 정보를 저장하는 테이블을 가진다.&lt;/li&gt;
&lt;li&gt;테이블에 저장된 정보에 따라 Request를 처리할 Method에 매핑한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ViewResolver&lt;/h4&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;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;Controller가 반환한 View Name을 바탕으로 적절한 View 객체 (Physical View Files) 를 반환한다.&lt;/span&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;View Name: home&lt;/li&gt;
&lt;li&gt;Prefix: /WEB-INF/views/&lt;/li&gt;
&lt;li&gt;Suffix: .jsp&lt;/li&gt;
&lt;li&gt;즉, &quot;/WEB-INF/views/home.jsp&quot; 위치의 View(JSP) 에 Controller에서 받은 Model을 전달하고, 해당 View에서 Model 데이터를 이용하여 적절한 페이지를 만들고 Client에게 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aridom.tistory.com/61&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://aridom.tistory.com/61&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&amp;amp;blogId=semi7623&amp;amp;logNo=100005637337&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&amp;amp;blogId=semi7623&amp;amp;logNo=100005637337&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://joont92.github.io/spring/DispatcherServlet-Flow/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://joont92.github.io/spring/DispatcherServlet-Flow/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gmlwjd9405.github.io/2018/12/20/spring-mvc-framework.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gmlwjd9405.github.io/2018/12/20/spring-mvc-framework.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/244</guid>
      <comments>https://soobarkbar.tistory.com/244#entry244comment</comments>
      <pubDate>Fri, 15 Apr 2022 15:01:25 +0900</pubDate>
    </item>
    <item>
      <title>(MAC) HomeBrew + JDK 8버전 설치</title>
      <link>https://soobarkbar.tistory.com/243</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HomeBrew 설치&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M1 칩 있는 맥북&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782265452&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://gist.githubusercontent.com/nrubin29/bea5aa83e8dfa91370fe83b62dad6dfa/raw/48f48f7fef21abb308e129a80b3214c2538fc611/homebrew_m1.sh)&quot;&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;M1 칩 없는 맥북&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782318568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&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;size18&quot;&gt;그런데 만약 M1 칩 있는 맥북에서 M1 칩 없는 맥북 설치 방법으로 한다면 아래와 같이 인식을 못한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648782372812&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;zsh: command not found: brew&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러면 아래 명령어를 입력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1648782400210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eval $(/opt/homebrew/bin/brew shellenv)&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;터미널을 켤 때마다 brew가 실행되도록 하는 방법&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;m1 칩 맥북
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vi ~/.zshrc의 마지막 줄에 아래 명령어 입력 후 저장 (:wq)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782723284&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eval $(/opt/homebrew/bin/brew shellenv)&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;m1 칩 없는 맥북
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vi ~/.zshrc에서 환경변수 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1648782800492&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export PATH=/usr/local/bin:/usr/local/sbin:$PATH&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;brew 설치 확인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;brew -v&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDK 8 설치&lt;/h2&gt;
&lt;pre id=&quot;code_1648782969971&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;brew tap AdoptOpenJDK/openjdk
 
brew install --cask adoptopenjdk8
 
/usr/libexec/java_home -v 1.8&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;java -version&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co6nXv/btryaG8FxlW/Vj1lvfq8JD9Kg75z2QEzA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco6nXv%2FbtryaG8FxlW%2FVj1lvfq8JD9Kg75z2QEzA0%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;500&quot; height=&quot;68&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;134&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;javac&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZ06c/btrx9AgLMdA/YDwCIwbvlsUFxD5I2rgAhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZ06c%2Fbtrx9AgLMdA%2FYDwCIwbvlsUFxD5I2rgAhK%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;600&quot; height=&quot;403&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tlo-developer.tistory.com/267&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tlo-developer.tistory.com/267&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://designdepot.tistory.com/209&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://designdepot.tistory.com/209&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/243</guid>
      <comments>https://soobarkbar.tistory.com/243#entry243comment</comments>
      <pubDate>Fri, 1 Apr 2022 12:19:27 +0900</pubDate>
    </item>
    <item>
      <title>Factory Method Pattern</title>
      <link>https://soobarkbar.tistory.com/241</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;팩토리 메서드 패턴?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;객체 생성 코드를 별도의 클래스 또는 메서드로 분리하여 객체 생성 변화에 효과적으로 대응할 수 있는 디자인 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;여러 대의 엘리베이터가 있다고 생각해보자. 사용자가 버튼 (FloorButton) 을 눌렀을 때, 여러 대의 엘리베이터 중 하나를 선택하여 이동시켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;ex) 목적지 층과 가까우면서 목적지 층의 방향으로 이동 중인 엘리베이터 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ziygn/btrinQFK49q/roXKk8lqSeY0z979h9mhZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ziygn/btrinQFK49q/roXKk8lqSeY0z979h9mhZ1/img.png&quot; data-alt=&quot;복수의 엘리베이터를 스케줄링해 엘리베이터를 이동시키는 클래스 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ziygn/btrinQFK49q/roXKk8lqSeY0z979h9mhZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fziygn%2FbtrinQFK49q%2FroXKk8lqSeY0z979h9mhZ1%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;1662&quot; height=&quot;638&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;복수의 엘리베이터를 스케줄링해 엘리베이터를 이동시키는 클래스 다이어그램&lt;/figcaption&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;ElevatorManager 클래스 : 이동 요청을 처리하는 클래스 (ThroughputScheduler 객체, ElevatorController 객체 복수 개를 갖는다.)&lt;/li&gt;
&lt;li&gt;requestElevator 메서드 : 요청 (목적지 층, 방향) 을 받았을 때, 우선 ThroughputScheduler 클래스의 selectElevator 메서드를 호출해 적절한 엘리베이터를 선택한 뒤, 선택된 엘리베이터에 해당하는 ElevatorController 객체의 gotoFloor 메서드를 호출해 엘리베이터를 이동시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1634718541932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ElevatorManager {
    private List&amp;lt;ElevatorController&amp;gt; controllers;
    private ThroughputScheduler scheduler;
    
    // 주어진 수만큼의 ElevatorController를 생성함
    public Elevatormanager(int controllerCount) {
        controllers = new ArrayList&amp;lt;&amp;gt;(controllerCount);
        
        for (int i = 0; i &amp;lt; controllerCount; ++i) {
            ElevatorController controller = new ElevatorController(i);
            controllers.add(controller);
        }
        
        scheduler = new ThroughputScheduler(); // ThroughputScheduler 객체를 생성함
    }
    
    void requestElevator(int destination, Direction direction) {
        // ThroughputScheduler를 이용해 엘리베이터를 선택함
        int selectedElevator = scheduler.selectElevator(this, destination, direction);
        
        // 선택된 엘리베이터를 이동시킴
        controllers.get(selectedElevator).gotoFloor(destination);
    }
}

public class ElevatorController {
    private int id;       // 엘리베이터 ID
    private int curFloor; // 현재 층
    
    public ElevatorController(int id) {
        this.id = id;
        curFloor = 1;
    }
    
    public void gotoFloor(int destination) {
        System.out.println(&quot;Elevator [&quot; + id + &quot;] Floor: &quot; + curFloor);
        
        // 현재 층 갱신, 즉 주어진 목적지 층 (destination) 으로 엘리베이터가 이동함
        curFloor = destination;
        System.out.println(&quot; ==&amp;gt; &quot; + curFloor);
    }
}

public class ThroughputScheduler {
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 현재 ElevatorManager 클래스는 ThroughputScheduler 클래스를 이용한다. 만약 다른 스케줄링 전략을 사용해야 한다면?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 다른 스케줄링인 대기 시간을 최소화하는 전략을 수행하기 위한 스케줄링 클래스가 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1637395487572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ElevatorManager {
    private List&amp;lt;ElevatorController&amp;gt; controllers;
    
    public ElevatorManager(int controllerCount) {
        controllers = new ArrayList&amp;lt;ElevatorController&amp;gt;(controllerCount);
        for (int i = 0; i &amp;lt; controllerCount; ++i) {
            ElevatorController controller = new ElevatorController(i + 1);
            controllers.add(controller);
        }
    }
    
    void requestElevator(int destination, Direction direction) {
        ElevatorScheduler scheduler;
        
        // 0..23
        int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
        
        if (hour &amp;lt; 12) // 오전에는 ResponseTimeScheduler를 이용함
            scheduler = new ResponseTimeScheduler();
        else
            scheduler = new ThroughputScheduler();
            
        int selectedElevator = scheduler.selectElevator(this, destination, direction);
        controllers.get(selectedElevator).gotoFloor(destination);
    }
}&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;1674&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PwBSj/btrlGJXE4aR/aDJpdvhUs0UqcokaPIafT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PwBSj/btrlGJXE4aR/aDJpdvhUs0UqcokaPIafT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PwBSj/btrlGJXE4aR/aDJpdvhUs0UqcokaPIafT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPwBSj%2FbtrlGJXE4aR%2FaDJpdvhUs0UqcokaPIafT0%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;1674&quot; height=&quot;888&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스트래티지 패턴을 사용하게 되면서 ElevatorManager 클래스는 ThroughputScheduler 또는 ResponseTimeScheduler 중 한 클래스를 동적으로 선택할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 ElevatorManager 클래스는 엘리베이터 스케줄링 전략이 변경될 때 requestElevator 메서드도 수정되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;/li&gt;
&lt;li&gt;requestElevator 메서드도 수정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&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;ex) 엘리베이터 스케줄링 전략에 일치하는 클래스를 생성하는 코드를 requestElevator 메서드에서 분리해 별도의 클래스 / 메서드를 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pOFzJ/btrlDB7BA5L/8BxA8iB1o8kfoQcnYNBcKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pOFzJ/btrlDB7BA5L/8BxA8iB1o8kfoQcnYNBcKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pOFzJ/btrlDB7BA5L/8BxA8iB1o8kfoQcnYNBcKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpOFzJ%2FbtrlDB7BA5L%2F8BxA8iB1o8kfoQcnYNBcKk%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;1678&quot; height=&quot;940&quot; data-origin-width=&quot;1678&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1637396673381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum SchedulingStrategyID { RESPONSE_TIME, THROUGHPUT, DYNAMIC }

public class SchedulerFactory {
    public static ElevatorScheduler getScheduler(SchedulingStrategyID strategyID) {
        ElevatorScheduler scheduler = null;
        
        switch (strategyID) {
            case RESPONSE_TIME: // 대기 시간 최소화 전략
                scheduler = new ResponseTimeScheduler();
                break;
            case THROUGHPUT:    // 처리량 최대화 전략
                scheduler = new ThroughputScheduler();
                break;
            case DYNAMIC:       // 오전에는 대기 시간 최소화 전략, 오후에는 처리량 최대화 전략
                int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
                if (hour &amp;lt; 12)  // 오전
                    scheduler = new ResponseTimeScheduler();
                else            // 오후
                    scheduler = new ThroughputScheduler();
                break;
        }
        
        return scheduler;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SchedulerFactory 클래스의 getScheduler 메서드는 인자로 주어진 SchedulingStrategyID에 따라 적절한 스케줄링 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 ElevatorManager 클래스의 requestElevator 메서드에서는 직접 스케줄링 클래스를 생성하는 대신 SchedulerFactory 클래스의 getScheduler 메서드를 호출하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1637396899921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ElevatorManager {
    private List&amp;lt;ElevatorController&amp;gt; controllers;
    private SchedulingStrategyId strategyID;
    
    public Elevatormanager(int controllerCount, SchedulingStrategyId strategyID) {
        controllers = new ArrayList&amp;lt;&amp;gt;(controllerCount);
        
        for (int i = 0; i &amp;lt; controllerCount; ++i) {
            ElevatorController controller = new ElevatorController(i);
            controllers.add(controller);
        }
        
        this.strategyID = strategyID; // 스케줄링 전략을 설정
    }
    
    public void setStrategyID(SchedulingStrategyID strategyID) {
        this.strategyID = strategyID;
    }
    
    void requestElevator(int destination, Direction direction) {
        // 주어진 전략 ID에 해당하는 ElevatorScheduler 사용
        ElevatorScheduler scheduler = schedulerFactory.getScheduler(strategyID);
        
        int selectedElevator = scheduler.selectElevator(this, destination, direction);
        controllers.get(selectedElevator).gotoFloor(destination);
    }
}

public class Client {
    public static void main(String[] args) {
        ElevatorManager emWithResponseTimeScheduler =
            new ElevatorManager(2, SchedulingStrategyID.RESPONSE_TIME);
        emWithResponseTimeScheduler.requestElevator(10, Direction.UP);
        
        ElevatorManager emWithThroughputScheduler =
            new ElevatorManager(2, SchedulingStrategyID.THROUGHPUT);
        emWithThroughputScheduler.requestElevator(10, Direction.UP);
        
        ElevatorManager emWithDynamicScheduler =
            new ElevatorManager(2, SchedulingStrategyID.DYNAMIC);
        emWithDynamicScheduler.requestElevator(10, Direction.UP);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 동일한 스케줄링 방식을 사용한다면 스케줄링 객체를 여러 번 생성하지 않고 한 번 생성한 것을 계속 사용하는 것이 바람직할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 싱글톤 패턴을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1638599965423&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SchedulerFactory {
    public static ElevatorScheduler getScheduler(SchedulingStrategyID strategyID) {
        ElevatorScheduler scheduler = null;
        
        switch (strategyID) {
            case RESPONSE_TIME:
                scheduler = ResponseTimeScheduler.getInstance();
                break;
            case THROUGHPUT:
                scheduler = ThroughputScheduler.getInstance();
                break;
            case DYNAMIC:
                int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
                if (hour &amp;lt; 12)
                    scheduler = ResponseTimeScheduler.getInstance();
                else
                    scheduler = ThroughputScheduler.getInstance();
                break;
        }
        
        return scheduler;
    }
}

// 싱글톤 패턴으로 구현한 ThroughputScheduler 클래스
public class ThroughputScheduler implements ElevatorScheduler {
    private static ElevatorScheduler scheduler;
    private ThroughputScheduler() { };
    
    public static ElevatorScheduler getInstance() {
        if (scheduler == null)
            scheduler = new ThroughputScheduler();
        
        return scheduler;
    }
    
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }
}

// 싱글톤 패턴으로 구현한 ResponseTimeScheduler 클래스
public class ResponseTimeScheduler implements ElevatorScheduler {
    private static ElevatorScheduler scheduler;
    private ResponseTimeScheduler() { };
    
    public static ElevatorScheduler getInstance() {
        if (scheduler == null)
            scheduler = new ResponseTimeScheduler();
        
        return scheduler;
    }
    
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>디자인 패턴</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/241</guid>
      <comments>https://soobarkbar.tistory.com/241#entry241comment</comments>
      <pubDate>Sat, 4 Dec 2021 15:48:05 +0900</pubDate>
    </item>
    <item>
      <title>Decorator Pattern</title>
      <link>https://soobarkbar.tistory.com/240</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;데코레이터 패턴?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&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;ex) 기본 도로 표시 기능 (+ 차선 표시, 교통량 표시, 교차로 표시, 단속 카메라 표시)&lt;/li&gt;
&lt;li&gt;4가지 추가 기능이 있을 때, 이 들의 조합은 총 15가지가 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데코레이터 패턴을 사용하면 이를 4가지 Decorator 클래스만 구현하여 객체 형태로 조합하여 추가 기능의 조합을 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;등장 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&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;/li&gt;
&lt;li&gt;추가 기능 : 도로의 차선을 표시하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;474&quot; width=&quot;250&quot; height=&quot;274&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bURDzx/btrhqgefhnt/qA3jmOlC1BMLKlVZ9K3Ya0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bURDzx/btrhqgefhnt/qA3jmOlC1BMLKlVZ9K3Ya0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bURDzx/btrhqgefhnt/qA3jmOlC1BMLKlVZ9K3Ya0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbURDzx%2Fbtrhqgefhnt%2FqA3jmOlC1BMLKlVZ9K3Ya0%2Fimg.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;474&quot; width=&quot;250&quot; height=&quot;274&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1633926662059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본 도로 표시 클래스
public class RoadDisplay {
    public void draw() {
        System.out.println(&quot;기본 도로 표시&quot;);
    }
}

// 기본 도로 표시 + 차선 표시 클래스
public class RoadDisplayWithLane extends RoadDisplay {
    public void draw() {
        super.draw();
        drawLane();
    }
    
    private void drawLane() {
        System.out.println(&quot;차선 표시&quot;);
    }
}

public class Client {
    public static void Main(String[] args) {
        RoadDisplay road = new RoadDisplay();
        road.draw(); // 기본 도로만 표시
        
        RoadDisplayWithLane roadWithLane = new RoadDisplayWithLane();
        roadWithLane.draw(); // 기본 도로 + 차선 표시
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 또 다른 도로 표시 기능을 추가할 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;교통량을 표시하는 클래스를 만들기 위해 RoadDisplayWithLane 클래스와 마찬가지로 RoadDisplay 클래스를 상속받아 RoadDisplayWithTraffic 클래스를 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;494&quot; width=&quot;450&quot; height=&quot;258&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvcWyS/btrhfy1OMS8/4jrzaQ89JkLT3eLvg1VEV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvcWyS/btrhfy1OMS8/4jrzaQ89JkLT3eLvg1VEV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvcWyS/btrhfy1OMS8/4jrzaQ89JkLT3eLvg1VEV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvcWyS%2Fbtrhfy1OMS8%2F4jrzaQ89JkLT3eLvg1VEV1%2Fimg.png&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;494&quot; width=&quot;450&quot; height=&quot;258&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1633927074302&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기본 도로 표시 + 교통량 표시 클래스
public class RoadDisplayWithTraffic extends RoadDisplay {
    public void draw() {
        super.draw();
        drawTraffic();
    }
    
    private void drawTraffic() {
        System.out.println(&quot;교통량 표시&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 추가 기능을 조합해야 하는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본 기능 클래스의 하위 클래스로 추가 기능을 만드는 것은 적절할 수도 있지만, &lt;b&gt;기능의 다양한 조합&lt;/b&gt;을 고려할 경우에 상속을 통한 기능의 확장은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;각 기능별로 클래스를 추가해야 한다는 단점&lt;/b&gt;&lt;/span&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본 기능에 추가로 사용할 수 있는 기능이 3가지 (차선 표시, 교통량 표시, 교차로 표시) 가 있다고 했을 때, 아래 표와 같이 8가지 조합이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;638&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWbYgp/btrhfzGnI3r/27jc1mA43WSjnsrXykFvPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWbYgp/btrhfzGnI3r/27jc1mA43WSjnsrXykFvPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWbYgp/btrhfzGnI3r/27jc1mA43WSjnsrXykFvPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWbYgp%2FbtrhfzGnI3r%2F27jc1mA43WSjnsrXykFvPk%2Fimg.png&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;638&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 상속을 통해 설계한다면 각 조합별로 하위 클래스를 구현해야 한다. 아래는 추가 기능의 조합을 설계한 모습이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;874&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biXJf6/btrhqgZCU37/KXphbbyDPtlqNW4Dkek0p0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biXJf6/btrhqgZCU37/KXphbbyDPtlqNW4Dkek0p0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biXJf6/btrhqgZCU37/KXphbbyDPtlqNW4Dkek0p0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiXJf6%2FbtrhqgZCU37%2FKXphbbyDPtlqNW4Dkek0p0%2Fimg.png&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;874&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;size18&quot;&gt;상속을 이용한 기능 추가 방법을 설명했지만, 이 방법은 추가 기능이 늘어날때마다 기능의 조합별로 하위 클래스를 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조합 수가 늘어나는 문제를 해결하기 위해서는 각 추가 기능별로 개별 클래스를 설계하고, &lt;b&gt;기능을 조합할 때 각 클래스의 &lt;span style=&quot;color: #ee2323;&quot;&gt;객체 조합&lt;/span&gt;을 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;736&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RBtCf/btrhd6K7RbX/TcILGiGJ2d9FHQpNZjVkC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RBtCf/btrhd6K7RbX/TcILGiGJ2d9FHQpNZjVkC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RBtCf/btrhd6K7RbX/TcILGiGJ2d9FHQpNZjVkC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRBtCf%2Fbtrhd6K7RbX%2FTcILGiGJ2d9FHQpNZjVkC1%2Fimg.png&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;736&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1633929613426&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class Display {
    public abstract void draw();
}

// 기본 도로 표시 클래스
public class RoadDisplay extends Display {
    public void draw() {
        System.out.println(&quot;기본 도로 표시&quot;);
    }
}

// 다양한 추가 기능에 대한 공통 클래스
public abstract class DisplayDecorator extends Display {
    private Display decoratedDisplay;
    
    public DisplayDecorator(Display decoratedDisplay) {
        this.decoratedDisplay = decoratedDisplay;
    }
    
    public void draw() {
        this.decoratedDisplay.draw();
    }
}

// 차선 표시 클래스
public class LaneDecorator extends DisplayDecorator {
    public LaneDecorator(Display displayDecorator) {
        super(displayDecorator);
    }
    
    public void draw() {
        super.draw();
        drawLane();
    }
    
    private void drawLane() {
        System.out.println(&quot;\t차선 표시&quot;);
    }
}

// 교통량 표시 클래스
public class TrafficDecorator extends DisplayDecorator {
    public TrafficDecorator(Display displayDecorator) {
        super(displayDecorator);
    }
    
    public void draw() {
        super.draw();
        drawTraffic();
    }
    
    private void drawTraffic() {
        System.out.println(&quot;\t교통량 표시&quot;);
    }
}&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;도로를 표시하는 기본 기능만 필요하다면 RoadDisplay 객체만을 이용하면 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차선 표시 기능이 필요하다면 RoadDisplay + LandDecorator&lt;/li&gt;
&lt;li&gt;교통량 표시 기능이 필요하다면 RoadDisplay + TrafficDecorator&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1633929905558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
        Display road = new RoadDisplay();
        road.draw(); // 기본 도로 표시
        
        Display roadWithLane = new RoadDisplay(new LaneDecorator());
        roadWithLane.draw(); // 기본 도로 표시 + 차선 표시
        
        Display roadWithTraffic = new RoadDisplay(new TrafficDecorator());
        roadWithTraffic.draw(); // 기본 도로 표시 + 교통량 표시
    }
}&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;road, roadWithLane, roadWithTraffic 객체의 접근 모두 Display 클래스를 통해 이루어진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 어떠한 기능을 추가하던 상관 없이 Client 클래스는 Display 클래스만을 이용하여 일관성 있는 방식으로 도로 정보를 표시할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;교차로를 표시하는 기능을 추가해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;954&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2jKrk/btrhgk3iHQU/oOPmrzlgazhisVjcyHUxoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2jKrk/btrhgk3iHQU/oOPmrzlgazhisVjcyHUxoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2jKrk/btrhgk3iHQU/oOPmrzlgazhisVjcyHUxoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2jKrk%2Fbtrhgk3iHQU%2FoOPmrzlgazhisVjcyHUxoK%2Fimg.png&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;954&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1633930361439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 교차로 표시 클래스
public class CrossingDecorator extends DisplayDecorator {
    public CrossingDecorator(Display displayDecorator) {
        super(displayDecorator);
    }
    
    public void draw() {
        super.draw();
        drawCrossing();
    }
    
    private void drawCrossing() {
        System.out.println(&quot;\t교차로 표시&quot;);
    }
}

public class Client {
    public static void main(String[] args) {
        Display roadWithLaneTrafficCrossing = new RoadDisplay(
            new LaneDecorator(new TrafficDecorator(new CrossingDecorator()))
        );
        roadWithLaneTrafficCrossing.draw(); // 기본 도로 표시 + 차선 표시 + 교통량 표시 + 교차로 표시
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도서 'UML과 GoF 디자인 패턴 핵심 10가지로 배우는 JAVA 객체 지향 디자인 패턴'&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>디자인 패턴</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/240</guid>
      <comments>https://soobarkbar.tistory.com/240#entry240comment</comments>
      <pubDate>Mon, 11 Oct 2021 14:38:46 +0900</pubDate>
    </item>
  </channel>
</rss>