티스토리 뷰
Redis는 무엇이며 장점은?
레디스는 키-값 구조의 비정형 데이터를 저장하고 관리하는 비관계형 DB입니다. 인메모리에 모든 데이터를 저장하여 데이터의 처리 속도가 매우 빠른 장점이 있습니다. 디스크 보다 메모리가 데이터 처리 속도가 빠른데 RDBMS는 대부분 디스크에 데이터를 저장하고 레디스는 메모리(RAM)에 데이터를 저장합니다.
Redis 주요 사용 사례
캐싱, 세션관리, 메시지 큐, 실시간 채팅 및 메시징 등 내장된 기능이 많아서 사용하는 용도가 많습니다.
레디스 설명은 이 정도만 하고 레디스에서 제일 많이 사용되는 캐시 적용 및 레디스의 클러스터 구성에 대해서 설명하겠습니다.
캐시, 캐싱이란?
원본 저장소 보다 빠르게 가져올 수 있는 임시 데이터 저장소를 의미를 캐시라고 하고 임시 저장소에 접근해서 데이터를 빠르게 가져오는 방식을 캐싱이라고 합니다. "데이터를 캐싱해 두고 사용하자"라는 의미입니다.
데이터를 캐싱할 때 사용하는 전략은 무엇이 있을까?
여러 전략 중에 Cache Aside(Look Aside), Write Around 전략에 대해서 간략하게 설명드리면 캐시에 데이터 확인하고 없다면 DB를 통해 조회해 오는 방식은 Cache Aside이고 사용자가 쓰기 작업을 하면 DB에만 반영하는 방식은 Write Around입니다. 레디스에 반영하지 않습니다. 두 조합으로 같이 썼을 때 캐시 데이터와 DB 데이터가 일치하지 않아 데이터의 일관성을 보장할 수 없는 한계점이 있습니다. 장시간 일치하지 않은 문제가 발생할 수 있기 때문에 적절한 주기로 TTL 설정을 통해 데이터를 동기화시켜야 합니다. 다른 전략 조합으로 구성했을 때 일관성을 보장하기 위해 데이터를 수정할 때마다 동시에 캐시도 수정하게 된다면 성능이 느려지는 단점이 있습니다. 어떤 전략의 조합이든 트레이트 오프가 발생하기 마련입니다. 그래서 캐시를 적용시키기 위한 데이터는 자주 조회되는 데이터, 변경이 자주 일어나지 않은 데이터 등 조건을 생각해서 적용하시면 됩니다.
캐싱 전 선행작업?
캐싱으로 조회 성능 개선하기 위해선 먼저 SQL 튜닝 작업을 먼저 고려해야 합니다. 추가적인 시스템을 구축은 비용이 발생하기 때문입니다. 조회가 느리다는 이유만으로 레디스 구축은 불필요할 수 있습니다. 근본적인 문제부터 해결한 후 결정하는 게 좋습니다.
Cache-Aside 요청 처리 단계
- 사용자가 데이터 요청 ➡️ 캐시 데이터 있음 ➡️ 캐시 데이터로 사용자 전달
- 사용자가 데이터 요청 ➡️ 캐시 데이터 없음 ➡️ DB에 데이터 요청 ➡️ 데이터 응답 ➡️ 레디스에 데이터 저장 ➡️ 사용자 전달
- 캐시에 데이터가 있으면 Cache Hit
- 캐시에 데이터가 없으면 Cache Miss
Redis 캐시 히트/미스 시퀀스 다이어그램
프로젝트에 레디스 실습
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager codeCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
.entryTtl(Duration.ofMinutes(1L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration).build();
}
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
}
서비스 코드
@Cacheable(
cacheNames = "getDetailCode",
key = "'detail-code:masterCd:' + #masterCd+ ':detailCd:' + #detailCd + ':useFlag:' + #useFlag",
cacheManager = "codeCacheManager")
public CdDetail getDetailCode(String masterCd, String detailCd, Boolean useFlag) {
CdDetail code = codeDetailRepository.findByMasterCdAndDetailCdAndUseFlag(masterCd, detailCd, useFlag)
.orElseThrow(() -> new CoreException(ErrorType.DETAIL_CODE_NOT_FOUND, detailCd));
return CdDetail.from(code);
}
Gradle 설정 및 Application.yml
implementation "org.springframework.boot:spring-boot-starter-data-redis"
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
도커 레디스 설치
version: '3.8'
services:
redis:
image: redis:7.2-alpine
container_name: bems-redis
ports:
- "6379:6379"
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./data/redis:/data
"{"@class":"com.example.codeservice.code.domain.dto.CdDetail",
"masterCd":"SM008","detailCd":"ROLE_USER","detailCdNm":"\xec\x82\xac\xec\x9a\xa9\xec\x9e\x90\"}"
위 설정을 통해 간단하게 로컬 환경에서의 단일 레디스 사용할 수 있습니다. 하지만 현업에선 직접적으로 레디스를 구축하는 게 아닌 ElastiCache를 사용합니다. 일일이 EC2에 Redis 설치하고 세팅하는데 신경 쓸게 많기 때문입니다.
RedisCacheConfig에서 serializeValuesWith new Jackson2JsonRedisSerializer<>(Object.class) 으로 변경해서 사용하게 된다면 아래 형식으로 받을 수 있습니다.
{"masterCd":"SM008","detailCd":"ROLE_USER","detailCdNm":"\xec\x82\xac\xec\x9a\xa9\xec\x9e\x90\"}
하지만 swagger에서 제가 여러번 클릭했을 때 테스트했을 때 아래와 같은 오류 로그 출력되었습니다. Redis가 저장된 데이터를 가져와서 자바 객체로 변환하는 과정에서 발생하는 타입 캐스팅(변환)라고 합니다.
class java.util.LinkedHashMap cannot be cast to class com.example.codeservice.code.domain.dto.CdDetail
(java.util.LinkedHashMap is in module java.base of loader 'bootstrap';
com.example.codeservice.code.domain.dto.CdDetail is
in unnamed module of loader 'app')
Redis에 데이터를 저장할 때 스프링 데이터 레디스는 Jackson2JsonRedisSerializer를 사용하여 객체를 JSON 형태로 직렬화합니다. 이때 객체의 타입 정보가 함께 저장되지 않으면 역직렬화 시 문제가 발생합니다. Jackson2JsonRedisSerializer<>(CdDetail.class)처럼 타입을 지정해 줄 수 있지만 사용 범위는 감소됩니다. GenericJackson2JsonRedisSerializer는 가장 간단하고 일반적인 방법입니다. 이 직렬화기는 별도로 타입을 지정할 필요 없이, 저장할 때 @class 정보를 JSON에 자동으로 포함시켜 역직렬화 시 변환 타입을 알 수 있게 해 줍니다.
지금까지 단일 레디스를 구성해봤습니다. 자주 조회되는 데이터를 DB까지 가지 않고 레디스에 캐싱해 빠르게 처리할 수 있다는 장점이 있습니다. 하지만 트래픽이 급증해 레디스 한 대가 과부하되어 장애가 나면 어떻게 될까요? 이 문제를 해결하는 방법이 레디스 클러스터입니다.
레디스 클러스터는 무엇이며 왜 필요한가?
여러 개의 레디스 노드를 묶어 하나의 DB처럼 묶어 쓰는 분산 시스템으로 샤딩과 고가용성을 동시에 제공합니다. Redis Sentinel는 고가용성만 제공하지만 레디스 클러스터는 해시 슬롯을 여러 마스터가 나눠 맡고 마스터마다 레플리카가 붙어 마스터가 장애 시 레플리카가 마스터로 자동 승격되어 무중단에 가깝게 운영할 수 있습니다. 또한 단일 레디스이면 CPU, RAM 한계로 지연이 커집니다. 클러스터는 노드가 늘수록 총 CPU, RAM이 합산되어 처리량이 증가하고 지연이 완화됩니다. 하지만 핫키 쏠림이면 효과 제한적입니다. 트래픽이 대부분이 한두 개 키로 물리면 그 마스터가 여전히 병목입니다. 키를 잘게 나뉘기 설계로 균등하게 분산을 유도해야 합니다.
클러스터의 핵심 원리
데이터 파티셔닝
레디스는 모든 데이터를 16384개의 해시 슬롯으로 나누고 각 슬롯을 클러스터 노드에 분배해 3개의 마스터 노드가 있다면 각 노드는 0~5460번, 5461~10922번, 10923~16383번을 담당합니다. 특정 키가 어떤 슬롯에 할당되는지는 CRC16 해시 함수를 사용해 결정합니다. CRC16 % 16384 연산으로 슬롯 번호를 계산하여 어떤 키가 어느 노드에 저장할지 고민할 필요 없습니다.
리다이렉션
클라이언트가 잘못된 노드에 데이터를 요청하면 해당 노드는 올바른 슬롯을 가진 노드의 주소로 클라이언트를 리다이렉션을 해줍니다. 이를 통해 클라이언트는 데이터를 직접 요청할 필요 없이 올바른 노드로 이동할 수 있습니다.
- MOVED: 소유 마스터가 바뀌어 영구적인 리다이렉트 / ASK: 재분배 중 일시적인 리다이렉트
레디스 클러스터 구성하기
환경: 레디스 도커 구성과 애플리케이션은 로컬환경
redis-7001:
image: redis:7.2-alpine
container_name: redis-7001
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7001 --cluster-announce-bus-port 17001
ports:
- "7001:7001" # 클라이언트 포트
- "17001:17001" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7001/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7001/data:/data
networks: [bems-network]
redis-7002:
image: redis:7.2-alpine
container_name: redis-7002
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7002 --cluster-announce-bus-port 17002
ports:
- "7002:7002" # 클라이언트 포트
- "17002:17002" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7002/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7002/data:/data
networks: [bems-network]
redis-7003:
image: redis:7.2-alpine
container_name: redis-7003
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7003 --cluster-announce-bus-port 17003
ports:
- "7003:7003" # 클라이언트 포트
- "17003:17003" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7003/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7003/data:/data
networks: [bems-network]
redis-7004:
image: redis:7.2-alpine
container_name: redis-7004
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7004 --cluster-announce-bus-port 17004
ports:
- "7004:7004" # 클라이언트 포트
- "17004:17004" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7004/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7004/data:/data
networks: [bems-network]
redis-7005:
image: redis:7.2-alpine
container_name: redis-7005
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7005 --cluster-announce-bus-port 17005
ports:
- "7005:7005" # 클라이언트 포트
- "17005:17005" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7005/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7005/data:/data
networks: [bems-network]
redis-7006:
image: redis:7.2-alpine
container_name: redis-7006
environment:
HOST_IP: "192.168.0.54"
command: redis-server /usr/local/etc/redis/redis.conf --cluster-announce-ip 192.168.0.54 --cluster-announce-port 7006 --cluster-announce-bus-port 17006
ports:
- "7006:7006" # 클라이언트 포트
- "17006:17006" # 클러스터 버스 포트(호스트에 개방 必)
volumes:
- ./7006/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7006/data:/data
networks: [bems-network]
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
redis-7001 Up 13 minutes 0.0.0.0:7001->7001/tcp, 6379/tcp, 0.0.0.0:17001->17001/tcp
redis-7003 Up 13 minutes 0.0.0.0:7003->7003/tcp, 6379/tcp, 0.0.0.0:17003->17003/tcp
redis-7004 Up 13 minutes 0.0.0.0:7004->7004/tcp, 6379/tcp, 0.0.0.0:17004->17004/tcp
redis-7002 Up 13 minutes 0.0.0.0:7002->7002/tcp, 6379/tcp, 0.0.0.0:17002->17002/tcp
redis-7005 Up 13 minutes 0.0.0.0:7005->7005/tcp, 6379/tcp, 0.0.0.0:17005->17005/tcp
redis-7006 Up 13 minutes 0.0.0.0:7006->7006/tcp, 6379/tcp, 0.0.0.0:17006->17006/tcp
각 노드용 redis.conf 파일 만들기
for p in 7001 7002 7003 7004 7005 7006; do
mkdir -p ./$p/data
cat > ./$p/redis.conf <<EOF
# --- 네트워킹 ---
port $p
bind 0.0.0.0
protected-mode no
# --- 클러스터 ---
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
# cluster-announce-* 는 compose에서 런타임 주입
# --- 영속화 ---
appendonly yes
appendfsync everysec
dir /data
EOF
done
6개의 Redis 노드를 묶어서 클러스터를 생성
docker exec -it redis-7001 redis-cli --cluster create \
192.168.0.54:7001 192.168.0.54:7002 192.168.0.54:7003 \
192.168.0.54:7004 192.168.0.54:7005 192.168.0.54:7006 \
--cluster-replicas 1 --cluster-yes
docker exec -it redis-7001 redis-cli -p 7001 cluster nodes
출력 내용
bd220643fb1a9c0185b4bfc4bfea73ea3b8b6e8f 192.168.0.54:7001@17001 myself,master - 0 1757490195000 1 connected 0-5460
a6275515fe5a523b203ef4d2908e76022afaacdc 192.168.0.54:7003@17003 master - 0 1757490194515 3 connected 10923-16383
6e6e2444096db8202006b23e9b3541a538d5fd88 192.168.0.54:7004@17004 slave 8d3cf65822a1bafb66611b84ade2bb045a9d3935 0 1757490195022 2 connected
458d15d76208a9db756a000eabbef3daf0d07041 192.168.0.54:7005@17005 slave a6275515fe5a523b203ef4d2908e76022afaacdc 0 1757490194000 3 connected
6eca274fd91a1e0253a827cd786e9e4d7f79a923 192.168.0.54:7006@17006 slave bd220643fb1a9c0185b4bfc4bfea73ea3b8b6e8f 0 1757490195535 1 connected
8d3cf65822a1bafb66611b84ade2bb045a9d3935 192.168.0.54:7002@17002 master - 0 1757490194009 2 connected 5461-10922
내용 요약
마스터
7001 → 슬롯 0-5460
7002 → 슬롯 5461-10922
7003 → 슬롯 10923-16383
리플리카(슬레이브)
7005 → 7001의 복제본
7006 → 7002의 복제본
7004 → 7003의 복제본
docker exec -it redis-7001 redis-cli -p 7001 cluster info
출력 내용
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:227
cluster_stats_messages_pong_sent:221
cluster_stats_messages_sent:448
cluster_stats_messages_ping_received:216
cluster_stats_messages_pong_received:222
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:443
total_cluster_links_buffer_limit_exceeded:0
내용 요약
cluster_state:ok, cluster_known_nodes:6, cluster_size:3
cluster_size:3 → 슬롯을 맡고 있는 마스터 노드 수
cluster_known_nodes:6 → 클러스터가 알고 있는 전체 노드 0수(마스터+리플리카)
cluster_slots_assigned:16384 → 모든 슬롯이 배정됐는지
cluster_state:ok → 슬롯 커버리지/헬스가 정상인지
CLUSTER SLOTS: 이 명령어는 현재 Redis 클러스터의 전체 슬롯 할당 상태를 보여줍니다.
docker exec -it redis-7001 redis-cli -p 7001 CLUSTER SLOTS
1) 1) (integer) 0
2) (integer) 5460
3) 1) "192.168.0.54"
2) (integer) 7001
3) "bd220643fb1a9c0185b4bfc4bfea73ea3b8b6e8f"
4) (empty array)
4) 1) "192.168.0.54"
2) (integer) 7006
3) "6eca274fd91a1e0253a827cd786e9e4d7f79a923"
4) (empty array)
2) 1) (integer) 5461
2) (integer) 10922
3) 1) "192.168.0.54"
2) (integer) 7002
3) "8d3cf65822a1bafb66611b84ade2bb045a9d3935"
4) (empty array)
4) 1) "192.168.0.54"
2) (integer) 7004
3) "6e6e2444096db8202006b23e9b3541a538d5fd88"
4) (empty array)
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "192.168.0.54"
2) (integer) 7003
3) "a6275515fe5a523b203ef4d2908e76022afaacdc"
4) (empty array)
4) 1) "192.168.0.54"
2) (integer) 7005
3) "458d15d76208a9db756a000eabbef3daf0d07041"
4) (empty array)
샤딩/ 라우팅 테스트
노드들이 기본적인 기능들을 제대로 수행하는지 확인 테스트를 의미로 해당 부분에 smoke:key 문자열을 넣고 문자열 CRC16 해시 값으로 계산후 % 16384 계산합니다. 아래 코드블록을 보면 smoke:key는 해시 계산을 통해 13880번 슬롯에 할당되어 있습니다. smoke:key에 값 1을 저장하려는 시도 합니다. MOVED 13880 192.168.0.54:7003 오류가 발생했습니다. MOVED 에러는 문제가 아니라 클러스터가 정상적으로 데이터를 라우팅 하고 있음을 나타내는 신호입니다. -c 옵션으로 자동 리다이렉트 할 수 있습니다. 해당 키의 슬롯에 맞게 마스터 노드에 잘 저장되어 있는 걸 확인할 수 있습니다.
// 해당 키는 슬롯이 13880
docker exec -it redis-7001 redis-cli -p 7001 CLUSTER KEYSLOT smoke:key
(integer) 13880
// 7001에 쿼리했지만 해당 키 슬롯은 7003 소유라 그쪽으로 보내달라고 리다이렉트
docker exec -it redis-7001 redis-cli -p 7001 SET smoke:key 1
(error) MOVED 13880 192.168.0.54:7003
// -c 옵션으로 자동 리다이렉트
docker exec -it redis-7001 redis-cli -c -p 7001 SET smoke:key 1
OK
// 소유 마스터(7003)에 직접 연결
docker exec -it redis-7003 redis-cli -p 7003 SET smoke:key 1
OK
// 값 조회
docker exec -it redis-7003 redis-cli -p 7003 GET smoke:key
"1"
장애 전환 테스트
마스터를 종료 시키면서 레플리카가 자동으로 마스터로 승격하는지 확인
// 레디스 7003 마스터 노드 정지
docker stop redis-7003
// 실행중인 마스터 노드 목록 조회
docker exec -it redis-7001 redis-cli -p 7001 cluster nodes | grep master
bd220643fb1a9c0185b4bfc4bfea73ea3b8b6e8f 192.168.0.54:7001@17001 myself,master - 0 1757556960000 1 connected 0-5460
a6275515fe5a523b203ef4d2908e76022afaacdc 192.168.0.54:7003@17003 master,fail - 1757556944897 1757556943000 3 disconnected
458d15d76208a9db756a000eabbef3daf0d07041 192.168.0.54:7005@17005 master - 0 1757556960240 7 connected 10923-16383
8d3cf65822a1bafb66611b84ade2bb045a9d3935 192.168.0.54:7002@17002 master - 0 1757556960754 2 connected 5461-10922
// 7005 복제본 노드가 마스터 노드로 승격
// 7003 노드 실행
docker start redis-7003
docker exec -it redis-7001 redis-cli -p 7001 cluster nodes | grep master
bd220643fb1a9c0185b4bfc4bfea73ea3b8b6e8f 192.168.0.54:7001@17001 myself,master - 0 1757557309000 1 connected 0-5460
458d15d76208a9db756a000eabbef3daf0d07041 192.168.0.54:7005@17005 master - 0 1757557310503 7 connected 10923-16383
8d3cf65822a1bafb66611b84ade2bb045a9d3935 192.168.0.54:7002@17002 master - 0 1757557309074 2 connected 5461-10922
// 7003 노드는 복제본으로 변경됨 7005 노드가 마스터 노드로 지속 운영
추가적인 궁금증
한 슬롯의 마스터와 레플리카가 동시에 죽는다면 어떻게 될까?
다른 슬롯들은 살아있습니다. 하지만 죽어있는 슬롯 범위가 unserved가 되어 자동 승격 후보가 없으니 전체 클러스터가 요청을 거절합니다. 마치 레디스가 전체 다운된 거처럼 보입니다. 이유는 cluster-require-full-coverage: yes이 기본값으로 설정되어 있어서입니다. 해당 설정값을 no로 변경하게 된다면 죽은 슬롯 외 마스터들은 계속 서비스가 가능합니다. 하지만 no으로 변경하더라도 마스터 노드가 절반 이상 죽으면 클러스터는 중단됩니다.
마스터 3개와 레플리카 3개의 관계
마스터 3개가 각 7001, 7002, 7003 레플리카 7004, 7005, 7006 순으로 연결했다면 7001이 죽으면 7004가 승격하지만 7001 마스터와 7004 레플리카가 둘 다 죽었을 경우 7005 레플리카가 승격할 수 없습니다.
레디스에 대해 이해하고 프로젝트로 테스트까지 마쳤습니다. 레디스 구축은 비용이 들지만 트래픽 급증으로 인한 DB 과부하, 장애를 완화하는 데 효과적인 선택입니다.
'Backend' 카테고리의 다른 글
왜 나는 RestClient를 선택했는가 (0) | 2025.09.23 |
---|---|
쿠버네티스 핵심 개념 정리와 마이크로서비스 이해 (0) | 2025.09.19 |
- Total
- Today
- Yesterday
- Lower
- Upper
- find
- for
- If
- Lambda
- zip
- operators
- bool
- Python
- index
- Method
- permutations
- combinations
- function
- isalpha
- Built-in Functions
- isdigit
- counter
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |