- 이 프로젝트는 Kotlin과 Spring을 사용하여 개발된 서버 애플리케이션입니다.
- Language: Kotlin 1.9 (JVM 17)
- Backend: Spring Boot 3.2.5, JPA
- Database: MySQL 8.0
- API Testing: JUnit5
- Containerization: Docker, Docker Compose
- 인메모리 데이터 그리드: Redisson
- Docker 실행 (Docker 설치 방법 : https://docs.docker.com/get-docker/)
- Docker Desktop을 설치하고 실행합니다.
- Docker Compose 를 설치하고 실행합니다.
docker-compose up -d - MySQL 서버 시작: MySQL 서버가 시작되면 애플리케이션은 MySQL을 사용하여 데이터를 저장합니다.
- Redis 서버 시작: 먼저 로컬 머신에서 Redis 서버를 시작해야 합니다. Redis 서버가 시작되면 애플리케이션은 Redis를 사용하여 분산 락을 구현합니다.
- DDL 실행
- (만약 테이블이 정상적으로 생성되지 않았다면) 아래 파일을 확인하여 MySQL 서버 내에 테이블을 생성합니다.
- /src/main/resources/sql/ddl.sql
- DML 실행
- /src/main/resources/sql/dml.sql
- 500 여개의 상품 데이터를 추가합니다.
- 애플리케이션 실행
./gradlew bootRun- 테스트코드 실행 확인
./gradlew test- API 호출: Postman API 문서를 사용하여 이러한 API를 호출할 수 있습니다.
- Postman API 문서 : API 문서
- 프로젝트를 Workspace로 Import하여 API 문서를 확인할 수 있습니다.
- API 문서에서는 회원가입, 로그인, 찜 서랍 조회, 찜 서랍 등록, 찜 서랍 삭제, 찜하기, 찜 목록 조회 API를 실제로 동작시켜 확인할 수 있습니다.
로그인 이후, 토큰을 발급받아 아래와 같이 헤더에 추가하여 API를 호출할 수 있습니다. (사진 참고)
발급 받은 토큰을 Authorization에 Bearer Token으로 추가하여 API를 호출합니다.
- 최종 구현된 범위입니다
- 회원가입 및 로그인 : 이메일과 비밀번호로 회원가입 및 로그인을 할 수 있습니다.
- 찜 서랍 조회, 등록 및 삭제 : 내 찜 서랍을 조회하고 등록, 삭제할 수 있습니다.
- 중복방지 : 단, 이미 있는 내 찜 서랍의 이름으로 생성할 수 없습니다.
- 찜 하기
- 상품을 찜하거나 해제 할 수 있습니다.
- 내 찜 서랍의 찜 목록을 볼 수 있습니다. 이 때, 페이지네이션 및 무한 스크롤로 탐색이 가능해야 합니다.
- 찜한 상품이 내 다른 찜 서랍에 있을 경우, 찜할 수 없습니다.
- 동시성 방지를 위해 분산락을 사용했습니다.
- 찜 서랍이 하나도 없을 경우, 상품을 찜할 수 없습니다.
- stateless한 서버 구조를 위해 토큰 기반 인증을 사용했습니다.
- 세션 기반 인증에 비해, 보안성은 떨어지나 서버의 확장성이 높은 구조이므로 사용
- Redis와 Kotlin의 Trailing Lambdas를 사용한 동시성 처리
- Redisson의 RLock으로 동시성 보장을 구현하면서 락이 획득되지 않는 상황을 고려해 타임아웃과 예외 처리를 포함한 안전한 분산 락 로직을 설계함
- Kotlin의 Trailing Lambdas를 활용해 락 획득-해제의 코드 구조를 간결하게 처리하고, 가독성을 개선함
- 락 구현을 공통화하여 찜 서랍 등록 외 다른 서비스에서도 재사용 가능하도록 설계한 점은 유지보수성과 확장성을 확보함
- 분산 락을 활용하면서도, 서비스 레벨에서만 적용함으로써 필요한 최소 범위에서만 락을 사용하는 최적화 설계를 적용함
- 멀티 유니크 키 제약 조건
- 찜 서랍의 이름과 회원 ID를 조합한 유니크 키 제약 조건을 통해, 동일한 회원이 동일한 찜 서랍을 중복 생성하는 것을 방지함
- 데이터베이스 레벨에서 유니크 키 제약 조건을 설정하여, 동시성 문제를 데이터베이스 레벨에서 해결함
- 40 여가지의 주요 케이스 검증
- 내 찜 서랍 등록, 삭제, 조회
- 찜 등록, 해제, 조회
- 로그인, 회원가입
- 테스트 코드 작성
- JUnit5를 사용하여 테스트 코드를 작성함
- 전체 테스트 코드 라인 커버리지 80% 이상 달성
- 주요 어플리케이션 로직에 대한 테스트 코드를 작성하여, 서비스 품질 보장
- Controller 레벨에서의 통합 테스트
- Service 레벨에서의 유닛 테스트를 작성하여 서비스 검증
- 찜 서랍 조회 API 성능 최적화
- 회원 ID를 기준으로 인덱스를 생성
- 전체 테이블을 검색하는 방식 대신, 인덱스를 활용하여 필요한 데이터를 빠르게 조회
CREATE TABLE IF NOT EXISTS wishlists
(
id bigint auto_increment
primary key,
name varchar(255) null,
user_id bigint not null,
created_at datetime(6) not null,
updated_at datetime(6) null
);
CREATE INDEX idx_wishlists_user_id
on wishlists (user_id);- 찜 목록 조회 API 성능 최적화
- 이를 위해 회원 ID와 찜 서랍 ID, 회원 ID에 각각 인덱스를 걸어줌
- 회원 ID를 기준으로 찜 목록을 조회할 수 있기 때문에, 회원 ID에 대한 인덱스를 추가함으로써 빠르게 조회 가능함
- 찜 서랍 ID에 대한 인덱스를 추가하여, 찜 서랍 ID를 기준으로 찜 목록을 조회할 수 있도록 함
- 상품 ID에 대한 인덱스를 추가하여, 상품 ID를 기준으로 찜 목록을 조회할 수 있도록 함
CREATE TABLE IF NOT EXISTS wishes
(
id bigint auto_increment
primary key,
created_at datetime(6) null,
product_id bigint not null,
updated_at datetime(6) null,
user_id bigint not null,
wishlist_id bigint not null,
is_deleted bit not null,
constraint wishes_product_id_user_id_uindex
unique (product_id, user_id)
);
CREATE INDEX idx_wishes_product_id
on wishes (product_id);
CREATE INDEX idx_wishes_user_id
on wishes (user_id);
CREATE INDEX idx_wishes_user_id_wishlist_id
on wishes (user_id, wishlist_id);
CREATE INDEX idx_wishes_wishlist_id
on wishes (wishlist_id);- 기타 테이블
CREATE TABLE IF NOT EXISTS users
(
id bigint auto_increment
primary key,
email varchar(255) not null,
name varchar(255) not null,
password varchar(255) not null,
constraint users_email_uindex
unique (email)
);
CREATE TABLE IF NOT EXISTS products
(
id bigint auto_increment
primary key,
name varchar(255) not null,
price decimal(38, 2) not null,
thumbnail varchar(255) null
);



