기술 검토 및 선정 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
-
Record 패턴 지원으로 간편한 DTO 설정 가능
- Record 패턴이란? 데이터만을 포함하는 불변한 객체로 선언할 수 있게 해주는 패턴
- Record 클래스는 14부터 사용 가능하나 패턴으로 사용 가능한건 21부터
-
LTS 중 최신으로 2031년까지 지원 예정
-
Virtual Thread 지원
JDK 21의 신기능 Virtual Thread 알아보기 (안정수 James)
[Project Loom] Virtual Thread에 봄(Spring)은 왔는가 | 카카오페이 기술 블로그
-
Virtual Thread(가상 스레드, 경량 스레드)란?
- (JAVA21 official docs) 특정 OS 스레드에 종속되지 않는 인스턴스, OS 스레드와 다수의 Virtual Thread가 매핑됨
- 기존 스레드는 실제 OS 스레드와 1:1 매핑되어 컨텍스트 스위칭 시 OS가 개입하는 등 비용이 높았음
- JVM 자체적인 스케쥴링으로 컨텍스트 스위칭 비용이 줄어 효율적으로 운영할 수 있음
- 블로킹 I/O 작업에 적합함
-
Virtual Thread 사용 시 유의사항(JAVA21 official docs)
- 가상 스레드 풀링 금지
- 가상 스레드는 자원의 개념보다는 작업의 개념으로 봐야 함. 플랫폼 스레드를 사용할 때는 자원을 미리 확보한다는 개념으로 풀링했지만, 가상 스레드는 풀링하면 비효율적임. 한 작업에 한 가상스레드!
- 제한적인 리소스 접근 및 사용 시 Semaphore를 사용한다.
- 세마포어 : 한 리소스에 최대 n개의 접근만 허용하는 기법
- DB의 경우 커넥션 풀이 세마포어의 역할을 함
- 재사용 가능 객체 유지하지 말 것
- 가상 스레드는 한 작업에 하나 할당됨. 캐싱하는 것은 해당 패턴에 위배
- 길고 빈번한 pinning 지양
- pinning : 가상 스레드가 특정 플랫퐄 스레드에 고정되어 스레드 하나를 점유하는 현상
- 가상 스레드 풀링 금지
-
무조건적인 Virtual Thread 도입이 아님, MVP 구축 후 병목 구간을 식별하여 도입하기 위해 필요
- 예상되는 사용 가능 구간 : 좌표를 kakao api로 행정구역으로 변환하는 과정
- official docs에 따르면, 10,000개 이상의 Virtual Thread를 만들 상황이 아니라면 Virtual Thread의 이점을 가질 수 없다고 함
-
SNS 특성 상 조회가 빈번할 것으로 예상됨, 이에 따른 대응책 구비 필요
-
JAVA 17과 비교
항목 Java 17 Java 21 출시 시점 2021.09 2023.09 지원 종료 2029 2031 ✅ LTS 여부 ✅ LTS ✅ LTS 기본 GC G1 GC G1 GC Generational ZGC 추가 Virtual Threads ❌ 미지원 ✅ 정식 지원 Record Patterns ❌ ✅ 정식 지원 Spring Boot 호환성 ✅ 안정 ✅ 최적 -
유저 한명이 최대 100장의 사진을 올릴 수 있고 AI서버 모델은 임베딩 2초, 후속작업(태깅, 중복사진 여부, 흔들린 사진 여부, 품질점수 측정) 1~2초 정도 소요
-
유저가 많아질 수록 처리해야하는 사진 수가 많아짐. ⇒ 대용량 처리 기술 도입 필요
-
AI서버는 비즈니스 로직에서 가장 긴 시간을 소모하여 DB접근과 동시에 병목이 예상되는 부분이므로 향후 AI서버 확장 시 Kafka로 병렬적이 확장 가능
- 자동화된 문서 생성으로 별도 문서생성 없이 컨트롤러 분석으로 API 스펙문서 작성 가능
- 자동으로 Swagger UI와 연동하여 FE와 협업 효율 증가
- 어노테이션 추가로 코드 작성하듯이 문서 생성 가능
-
컨트롤러에 어노테이션 기반 설명으로 주석으로 설명하는 것보다 가독성 향상
@Operation(summary = "유저 프로필 조회", description = "AccessToken을 기반으로 유저 정보를 조회합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "401", description = "Access Token이 유효하지 않음") }) @GetMapping("/api/user/{userId}") public ResponseEntity<UserResponse> getUserInfo() { ... }
-
-
Record 패턴 지원으로 간편한 DTO 설정 가능
- Record 패턴이란? 데이터만을 포함하는 불변한 객체로 선언할 수 있게 해주는 패턴
- Record 클래스는 14부터 사용 가능하나 패턴으로 사용 가능한건 21부터
-
LTS 중 최신으로 2031년까지 지원 예정
-
Virtual Thread 지원
[JDK 21의 신기능 Virtual Thread 알아보기 (안정수 James)](https://www.youtube.com/watch?v=vQP6Rs-ywlQ)
[Core Libraries](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html)
[[Project Loom] Virtual Thread에 봄(Spring)은 왔는가 | 카카오페이 기술 블로그](https://tech.kakaopay.com/post/ro-spring-virtual-thread/)
-
Virtual Thread(가상 스레드, 경량 스레드)란?
- (JAVA21 official docs) 특정 OS 스레드에 종속되지 않는 인스턴스, OS 스레드와 다수의 Virtual Thread가 매핑됨
- 기존 스레드는 실제 OS 스레드와 1:1 매핑되어 컨텍스트 스위칭 시 OS가 개입하는 등 비용이 높았음
- JVM 자체적인 스케쥴링으로 컨텍스트 스위칭 비용이 줄어 효율적으로 운영할 수 있음
- 블로킹 I/O 작업에 적합함
-
Virtual Thread 사용 시 유의사항(JAVA21 official docs)
- 가상 스레드 풀링 금지
- 가상 스레드는 자원의 개념보다는 작업의 개념으로 봐야 함. 플랫폼 스레드를 사용할 때는 자원을 미리 확보한다는 개념으로 풀링했지만, 가상 스레드는 풀링하면 비효율적임. 한 작업에 한 가상스레드!
- 제한적인 리소스 접근 및 사용 시 Semaphore를 사용한다.
- 세마포어 : 한 리소스에 최대 n개의 접근만 허용하는 기법
- DB의 경우 커넥션 풀이 세마포어의 역할을 함
- 재사용 가능 객체 유지하지 말 것
- 가상 스레드는 한 작업에 하나 할당됨. 캐싱하는 것은 해당 패턴에 위배
- 길고 빈번한 pinning 지양
- pinning : 가상 스레드가 특정 플랫퐄 스레드에 고정되어 스레드 하나를 점유하는 현상
- 가상 스레드 풀링 금지
-
무조건적인 Virtual Thread 도입이 아님, MVP 구축 후 병목 구간을 식별하여 도입하기 위해 필요
- 예상되는 사용 가능 구간 : 좌표를 kakao api로 행정구역으로 변환하는 과정
- official docs에 따르면, 10,000개 이상의 Virtual Thread를 만들 상황이 아니라면 Virtual Thread의 이점을 가질 수 없다고 함
-
SNS 특성 상 조회가 빈번할 것으로 예상됨, 이에 따른 대응책 구비 필요
-
JAVA 17과 비교
항목 Java 17 Java 21 출시 시점 2021.09 2023.09 지원 종료 2029 2031 ✅ LTS 여부 ✅ LTS ✅ LTS 기본 GC G1 GC G1 GC Generational ZGC 추가 Virtual Threads ❌ 미지원 ✅ 정식 지원 Record Patterns ❌ ✅ 정식 지원 Spring Boot 호환성 ✅ 안정 ✅ 최적
- JAVA21 호환성 최적
- 3.2와 비교 시 Virtual Thread 지원 개선(Micrometer Metric 시스템, Undertow 서버같은 구성요소가 Virtual Thread에서 제대로 동작하도록 개선) ⇒ Virtual Thread에 대한 호환성 강화
- Spring Framework 6.x 기반으로
jakarta.*
공식 지원(3.x부터 지원하긴 함) - HTTP 통신기능 향상
- 자동 HTTP 클라이언트 팩토리 선택 로직이 개선됨(Apache, Jetty, Netty 설정방식 통합)
- 별도 의존성을 추가하지 않으면 JDK 기존 HttpClient를 사용하도록 Default Inversion
- 테스트 편의성 증가
- Test Container로 DB를 띄울 때
@AutoConfigureTestDatabase
를 통해 유연한 테스트db 사용 가능
- Test Container로 DB를 띄울 때
- OAuth 사용 시 간편한 연동 가능
- JWT 사용 시 Bearer Token을 필터에서 처리 가능
- 사용자 권한 체크 한 줄로 표현 가능, 비밀번호 암호화 알고리즘 내장 등 보안에 유용한 도구 사용 가능
- 보일러플레이트 코드 제거를 통한 유지보수성 극대화
- 단순 반복 제거로 생산성 향상
- 설계 의도를 어노테이션으로 표현 가능
항목 | Spring Data JPA | MyBatis |
---|---|---|
추상화 수준 | 높음 (ORM) | 중간 (SQL 매핑) |
Spring 연동성 | 기본 연동 (Spring Data JPA) | 사용 가능 |
튜닝 자유도 | 제한적 (QueryDSL 사용 필요) | SQL 직접 제어 |
러닝 커브 | 이미 사용경험 있어 학습 난이도 낮음 | SQL 직접 제어로 난이도 높을 것으로 예상 |
특징 | 객체지향적인 설계, 연관관계 자동 관리 | SQL 중심, XML/어노테이션 매핑 |
- JPA 구현체 중 하나, Spring Data JPA와 기본 연동됨
- SQL을 직접 제어하지 않아 성능 튜닝에 제한적이지만 QueryDSL로 커버 가능
- 객체지향적 설계 지원으로 타 ORM 프레임워크와 비교할 때 러닝 커브 적을 것으로 예상
- 스프링부트와의 자연스러운 통합과 높은 호환성으로 엔티티 사용 시 에러이슈가 적을 것으로 예상됨
항목 | MySQL | PostgreSQL |
---|---|---|
주 사용처 | ✅ 대규모 읽기 트래픽 | 복잡한 비즈니스 및 데이터 모델 |
쿼리 성능 | 단순 SELECT문에서 빠름 | ✅ 쿼리 복잡할수록 유리함 |
인덱스 | 일반 인덱스가 빠름 | |
Partial, Expression 인덱스 가능하지만 유연하지 않음 | 복합, Partial, Expression 인덱스 가능 |
- 현재 온기서비스는 복잡한 비즈니스 로직 없음
- SNS 특성상 읽기 쿼리가 많을 것으로 예상됨 ⇒ MySQL이 SELECT문에서 빠름
- 앨범 조회, 앨범 피드 조회 등등..
- 무한스크롤을 통해 반복적인 읽기 api 호출 → 잦은 SELECT 쿼리 발행
- 수명이 짧은 access token 특징 상 인증 및 인가 흐름에서 refresh token의 유효성 검사는 자주 일어나므로 낮은 지연시간이 중요함
- Redis의 경우 실제 요청 처리 시 디스크 대신 메모리에서 조회하므로 응답속도가 향상됨
- Key별로 TTL(Time To Live) 설정 가능하여 refresh token 만료시간 효율적으로 설정 가능
구분 | Redis | Memcached |
---|---|---|
범용성 | 높음 (올인원) | 낮음 (단일 목적) |
성능 | 다양한 기능 + 빠름 | 초경량 + 매우 빠름 |
확장성 | 클러스터, HA 지원 | 단순 분산 (client-side) |
데이터 유지 | 영속 가능(AOF, RDB) | 휘발성 전용 |
- Redis는 Memcached와 다르게 다양한 데이터 타입을 넣을 수 있음
- 장애상황 대비하여 영속성 일부 유지 가능(AOF, RDB 활용)
- 클러스터링 및 분산 구성이 잘되어 있어 수평 확장에 용이함
- 향후 여러 인스턴스가 리프레시 토큰을 공유해야할 때 중앙 집중형 캐시 서버로 Redis 사용 가능
- 본 프로젝트 사용 시나리오
-
팔로우/언팔로우
-
내가 팔로우한 유저 ID, 나를 팔로우한 유저 ID를 SET으로 Redis 저장
Key: follow:following:<내_유저ID> Type: Set Value: [내가 팔로우한 유저 ID들] 예: follow:following:1001 → {2002, 2003, 2004} Key: follow:follower:<내_유저ID> Type: Set Value: [나를 팔로우한 유저 ID들] 예: follow:follower:2002 → {1001, 1010}
-
팔로우/팔로우 취소 시 DB에 반영 후 Redis에 캐싱
-
팔로우/팔로워 리스트 조회 시 Redis에서 먼저 조회 → 없으면 DB에서 조회
-
-
유저별 월별 방문 장소, 등록 사진 수
-
유저 월별 일간 사진 수와 전체 사진 수 Hash, String으로 저장
Key: stat:picture:daily:<userId>:2025-04 Type: Hash Value: { "2025-04-01": 3, "2025-04-02": 7, ... } Key: stat:picture:total:<userId> Type: String Value: "153"
-
유저 월별 장소 및 전체 방문 장소 수 Hash, String 저장
Key: stat:place:monthly:<userId>:2025-04 Type: Hash Value: { "강원도/횡성군/횡성읍": 10, "서울특별시/마포구/서교동": 5, ... } Key: stat:place:total:<userId> Type: String Value: "27"
-
-
피드 좋아요 표시
-
피드 좋아요 수와 좋아요 누른 유저 캐싱(INT, SET)
Key: like:feed:<feedId> Type: Set Value: [userId, userId, ...] Key: like:feed:count:<feedId> Type: String or Integer Value: 좋아요 수 (INT)
-
-
-
서비스 도입 시 Redis Stream과 Redis pub/sub 비교
구분 Redis Stream Redis pub/sub 메시지 저장 메시지 저장(디스크 보관 가능) 저장하지 않음(즉시 소멸) 유저별로 구분 key 기반 유저별 큐 분리 가능 채널 단위 브로드캐스트(전체공지만 가능) 읽음 여부 관리 추적 가능(XACK, XPENDING) 불가능 오프라인 수신 여부 온라인일 때 수신 가능 온라인 아니면 메시지 손실 - 추가로, 이미 캐싱 목적으로 Redis를 도입했기 때문에 Redis Stream으로 기능확장을 하는 것이 올바른 선택
-
Kafka는 도입 고려안한 이유?
[Redis Stream 적용기](https://dev.gmarket.com/113)
- Redis Stream 적용기를 찾다가 발견한 지마켓 테크블로그에서 kafka를 고려한 내용이 나온다.
- 지마켓도 kafka나 MQ를 고려했으나 개발 공수와 리소스가 많이 소모될 것이라고 판단하여 선택지에서 제외한다.
-
Kafka vs Redis Stream 비교
항목 Kafka Redis Stream 메시지 순서 보장 파티션 내 순서 보장 ID 기반 순서 보장 재시도 / 실패 복구 offset 기반 재처리 가능 XPENDING/XACK으로 처리 운영 복잡도 높음 (Zookeeper, 설정 필요) 낮음 (기존 Redis 사용 시 간단) 모니터링 도구 풍부함 (Kafka UI, Grafana 등) 제한적 (XINFO 등 직접 구현) 적합한 메시지 크기 수 MB 이상 가능 수 KB ~ 수십 KB 적절 파티션 기반 분산 자동 분산 없음 병렬 소비 확장 컨슈머 수 제한 거의 없음 그룹 수 늘어나면 병목 가능 처리량 증가 시 브로커/파티션 확장으로 해결 키 분할 등 수작업 필요 -
유저 한명이 최대 100장의 사진을 올릴 수 있고 AI서버 모델은 임베딩 2초, 후속작업(태깅, 중복사진 여부, 흔들린 사진 여부, 품질점수 측정) 1~2초 정도 소요
-
유저가 많아질 수록 처리해야하는 사진 수가 많아짐. ⇒ 대용량 처리 기술 도입 필요
-
AI서버는 비즈니스 로직에서 가장 긴 시간을 소모하여 DB접근과 동시에 병목이 예상되는 부분이므로 향후 AI서버 확장 시 Kafka로 병렬적이 확장 가능
- 자동화된 문서 생성으로 별도 문서생성 없이 컨트롤러 분석으로 API 스펙문서 작성 가능
- 자동으로 Swagger UI와 연동하여 FE와 협업 효율 증가
- 어노테이션 추가로 코드 작성하듯이 문서 생성 가능
-
컨트롤러에 어노테이션 기반 설명으로 주석으로 설명하는 것보다 가독성 향상
@Operation(summary = "유저 프로필 조회", description = "AccessToken을 기반으로 유저 정보를 조회합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "401", description = "Access Token이 유효하지 않음") }) @GetMapping("/api/user/{userId}") public ResponseEntity<UserResponse> getUserInfo() { ... }
-