
선착순 쿠폰을 발급하는 상황에서 데드락 문제와 동시성 문제를 해결한 과정과 성능 개선 과정에 대해서 글을 작성해보겠다. 1. 요구 사항이벤트 시작 시 수천 명에서 수만 명이 동시에 쿠폰을 요청하는 상황요구 사항 : 쿠폰 1000개 한정, 1인 1매 발급2. 데드락 문제 해결 과정아래 테이블 구조는 보기 편하게 기존 테이블 구조에서 불필요한 칼럼은 제외했다. (username과 name도 필요없긴 하다.)회원이 쿠폰 발급 버튼을 클릭하면 coupons 테이블에 있는 쿠폰 수량이 줄어들고, user_coupons 테이블에 발급한 쿠폰이 추가된다. 하지만 이 구조에서는 데드락이 걸리는 문제가 있다.쿼리문으로 상황을 살펴보도록 하자.-- transaction ABEGIN; -- 1INSERT INTO user..

기존 잉크 브릿지 프로젝트는 생일 쿠폰을 매월 1일 00:00에 배치로 발급하는 기능이 구현되어 있었고,이 로직은 팀원 분께서 Spring Batch 기반으로 개발했다. 이 기능의 요구 사항은 다음과 같았다.사용자가 생일인 달 1일 자정에 생일 쿠폰 제공예) {5월 1일, 5월 5일, 5월 20일} 생일자 : 5월 1일에 쿠폰 제공쿠폰 사용 기간은 해당 월의 1일부터 말까지여기서 나는 실제 이커머스(ex. 무신사?) 운영 환경을 고려해 다음과 같은 조건을 가정하고 요구사항을 확장했다.전체 회원 수는 약 2,400만 명각 달의 생일자는 약 200만 명씩 균등 분포그리고 개발된 코드를 리뷰하던 중, 성능 측면에서 개선이 필요한 부분들이 눈에 띄었고, 이를 기반으로 배치 처리 로직을 리팩토링하게 되었다.1. ..

InkBridge 프로젝트를 리팩토링하던 중, 스프링 배치를 적용해야 할 시점이 왔었다.그래서 스프링 배치 공부를 5버전으로 했는데 스프링 부트 3 이상에서만 이 버전을 지원한다는 것을 알았다.원래는 전반적인 리팩토링을 끝낸 뒤 마이그레이션을 진행하려고 했지만, 배치를 5버전으로 공부한 김에 지금 계획했던 마이그레이션을 하기로 했다. 마이그레이션 대상은 다음과 같다.Java 11 -> Java 17Maven -> GradleSpring Boot 2.7 -> Spring Boot 3.4현재 스프링 부트의 가장 최신 버전인 3.4 버전으로 마이그레이션 하기로 했고, 그에 따라 자바 버전도 17이상으로 올려야 했다.그리고 기존 빌드 도구는 Maven이었는데 이 팀 프로젝트를 진행한 이후로는 Gradle만 사용..
![[프로그래머스] 두 큐 합 같게 만들기 - JAVA](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FpP9ZD%2FbtsNRddSNEn%2FAAAAAAAAAAAAAAAAAAAAAEly0qM4Vb7MB-k0fMvyDERFNe7pIUglNHWPrNm3oK2y%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3D74Ruub0fMLAlF%252BVA7z%252B1EUNpR8g%253D)
문제 - 두 큐 합 같게 만들기 프로그래머스SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프programmers.co.kr처음 해결 방법import java.util.*;class Solution { public int solution(int[] queue1, int[] queue2) { Queue q1 = new ArrayDeque(); Queue q2 = new ArrayDeque(); long sum1 = 0; long sum2 = 0; for (long v : queue1) { q1.offer(v); sum1 += v; ..
![[프로그래머스] [1차] 비밀지도 - JAVA](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FDNWJA%2FbtsNB5trEMa%2FAAAAAAAAAAAAAAAAAAAAADJdO1FXpnN7F0oZu4iB1bhbytdwqnHHFfpGMmq-RLjV%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3DfWoLOytqeiGGMmhB2w6fOfsC%252BUU%253D)
문제 - [1차] 비밀지도처음 해결 방법class Solution { public String[] solution(int n, int[] arr1, int[] arr2) { String[] answer = new String[n]; int[] map = new int[n]; for(int i=0; i 처음에는 다음과 같은 흐름으로 문제를 해결했다.각 행을 비트연산(OR)으로 병합한다.병합된 값을 2진수 문자열로 변환한다.1을 #으로 변환하고, 0을 " "으로 변환한다.변환한 문자열이 n자리보다 짧으면 앞에 " "을 추가해 길이를 맞춘다.문제점 발견이 방법은 동작은 하지만 비효율적인 부분이 있었다.replace()는 내부적으로 문자열 전체를 순회하므로 시간복잡도 ..

save()와 saveAll()이 같은 저장 기능을 수행하지만 성능에는 차이가 있고 @Transactional 사용 여부에 따라서도 다른 결과를 나타낸다. 이번 글에서는 이 둘의 내부 구조와 함께, @Transactional 유무에 따른 성능 차이를 실험과 함께 분석해보겠다.1. save()와 saveAll() 내부 구조JpaRepository의 기본 구현체인 SimpleJpaRepository의 내부를 보면 다음과 같다. save()@Transactional@Overridepublic S save(S entity) { Assert.notNull(entity, "Entity must not be null."); if (entityInformation.isNew(entity)) { ..

기존에 구현한 카테고리 로직에서는 N+1 문제, 재귀적 구조의 한계, 조회 효율성 저하 같은 문제점들이 있었다.그래서 이번 글에서는 카테고리 리팩토링 과정을 담아보도록 하겠다.1. 기존 구조의 문제점1.1 인접 리스트 모델(Adjacency List Model)기존 구조는 인접 리스트 모델로, Category 엔티티 내에 자기 자신을 참조하는 CategoryParent, CategoryChildren 필드를 통해 재귀적으로 자기 자신을 참조했다.1.2 N+1 문제 발생readAllCategory() 메서드에서 부모 카테고리를 조회한 뒤, 각 카테고리의 자식들을 DTO로 변환하는 과정에서 지연 로딩으로 인해 매번 추가 쿼리가 발생했다. 여기서는 @BatchSize설정을 사용한다고 하더라도 완전한 최적화는 ..

댓글이나 카테고리 등 계층 구조를 가진 데이터를 표현할 때 보통 테이블을 재귀적으로 설계한다.하지만 계층 구조를 만드는 방식은 여러 가지가 있으며, 각각 장단점이 존재한다.이번 글에서는 대표적인 계층 구조 테이블 설계 방식들에 대해 알아보겠다.재귀구조를 띄는 인접리스트 모델중첩 집합 모델(Nested Set Model)클로저 테이블Materialized Path1. 인접리스트 모델 (Adjacency List Model)1.1 구조 설명가장 널리 사용되는 방식으로 각 데이터가 자신의 부모 ID를 참조한다.SQL로는 자기 참조 외래키를 통해 표현한다.CREATE TABLE category ( id BIGINT PRIMARY KEY, name VARCHAR(100) NOT NULL, parent_id ..

1. 상황과 문제점예시 Entitypublic class CategoryEntity extends BaseEntity { ... @Column(name = "name", nullable = false, length = 20) private String name; ... @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private CategoryEntity parentCategoryEntity; ...}예시 쿼리 메소드public interface CategoryJpaRepository extends JpaRepository { boolean existsByParentCategoryEn..

책 정보나 작가 정보, 리뷰 등을 등록할 때, 이미지 파일을 함께 저장해야 한다.이미지 파일은 DB와는 다른 IO 작업이라서 어떻게 로직을 구성할지가 중요했다.간단하게 생각하면 "이미지 업로드 → DB 저장" 또는 "DB 저장 → 이미지 업로드" 중 하나를 선택하면 될거라고 생각할 수도 있지만, 트랜잭션 처리, 예외 상황, 서버 부하 등 여러 요소를 함께 고려해야한다. 그래서 이 글에서는 내가 고민한 부분들과 해결 과정을 공유해보겠다.1. 사진 업로드와 DB 저장의 순서에 대한 고민1.1 하나의 요청 안에서 사진 업로드 → DB 저장이 방식은 파일을 먼저 업로드한 뒤, 그 결과로 받은 URL 혹은 파일식별자를 DB에 저장하는 구조다.장점이미지 업로드를 실패하면 DB 작업을 진행할 필요가 없어서 트랜잭션 ..

요청 DTO에서 ConstraintValidator로 Enum 검증하기클라이언트에서 데이터를 넘겨줄 때 @NotBlank, @NotEmpty 등으로 대부분의 유효성 검사를 할 수 있다.하지만 나의 경우에는 Enum에 바인딩 될 수 있는 값인지 검증을 해야했다. 그래서 이번 글에서는 ConstraintValidator를 활용해 Enum에서 지원하는 값인지 검증하는 방법을 공유해보겠다.1. 첫 번째 시도 : 바인딩 이후 도메인 변환 시점에서 Enum 검증처음에는 요청으로 들어온 문자열이 유효한 Enum 값인지 검증할 때, 아래와 같이 도메인 변환 로직 안에서 직접 처리했었다. 예시 Request 클래스DomainStatus.class 응답 이렇게 처리하면 올바르게 예외 처리를 할 수 있다.하지만 이 방식..

주소 도메인의 구조에 문제점이 있다고 판단하여 리팩토링을 진행했다.이 글에서는 ERD와 코드 구조를 개선하고, 어떤 문제를 어떻게 해결했는지 전반적인 리팩토링 과정을 작성해보겠다.1. 리팩토링 내용2개로 나뉘어 있던 테이블 -> 단일 테이블기존 코드 구조 개선 및 기능 추가주소 최대 10개 제한 로직 추가(기존 요구사항에 있던 내용)기본 배송지 설정 로직 추가(개인적인 추가)동시성 문제 해결계층 단위로 나뉘어진 패키지 구조 -> 도메인 기반 패키지 구조추후 모듈 분리를 고려한 구조로 설계2. 기존 ERD 구조 기존 주소 도메인은 general_address, member_address 두 테이블로 분리되어 있었다.아마 아파트 등의 공통 주소를 공유하는 사용자를 고려한 정규화로 추정된다.하지만 이 구조에 ..