Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
## [본 과정] 이커머스 핵심 프로세스 구현
[단기 스킬업 Redis 교육 과정](https://hh-skillup.oopy.io/) 을 통해 상품 조회 및 주문 과정을 구현하며 현업에서 발생하는 문제를 Redis의 핵심 기술을 통해 해결합니다.
> Indexing, Caching을 통한 성능 개선 / 단계별 락 구현을 통한 동시성 이슈 해결 (낙관적/비관적 락, 분산락 등)

## 프로젝트 실행방법

프로젝트를 받은 뒤
> $ docker compose up -d

위 명령어를 실행하면 schema와 data가 자동으로 생성됩니다.

그 후 api 모듈에 ApiApplication을 실행하면 프로젝트가 정상적을 작동합니다.

## 멀티모듈 구성
이 프로젝트는 **레이어드 아키텍처**를 기반으로 설계되었으며, 다음과 같이 3개의 모듈로 구성되어 있습니다.

### **API 모듈**
- **역할**
사용자 및 외부 시스템과의 상호작용을 담당하며, HTTP 요청을 처리하고 응답을 반환하는 역할을 합니다.
- **주요 책임**
- RESTful API 제공
- 요청 데이터 검증 및 변환
- Application 모듈에 의존하여 비즈니스 로직 호출
- **의존성**
- `application` 모듈

---

### **Application 모듈**
- **역할**
비즈니스 로직을 구현하는 핵심 계층으로, 도메인 모델과 외부 인터페이스(API 모듈)를 연결합니다.
- **주요 책임**
- 비즈니스 규칙 및 유스케이스 처리
- 트랜잭션 관리
- Domain 모듈과의 상호작용
- **의존성**
- `domain` 모듈

---

### **Domain 모듈**
- **역할**
프로젝트의 핵심 엔티티와 데이터 접근 로직을 관리합니다.
- **주요 책임**
- 도메인 엔티티 정의
- JPA 및 데이터베이스 접근
- 도메인 규칙을 보장하는 로직 포함
- **의존성**
- 독립적이며 다른 모듈에 의존하지 않음

---

## 테이블 설계
- **도메인**
![](https://velog.velcdn.com/images/kimbro97/post/060397e0-2ff7-4997-93d7-5e7c9c5da022/image.png)
- **테이블**
![](https://velog.velcdn.com/images/kimbro97/post/f3028e77-5355-476c-86e6-98c9c01b1aca/image.png)

Movie(영화)와 Theater(극장)은 M:N 관계이므로 MovieTheater로 일대다 다대일 관계로 설계하였습니다.
Movie(영화)와 Screening(상영)은 1:N 관계이므로 일대다 관계로 설계하였습니다.
Screening(상영)과 Seat(좌석)은 1:N 관계이므로 일대다 관계로 설계하였습니다.
4 changes: 4 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation project(':application')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
13 changes: 13 additions & 0 deletions api/src/main/java/com/example/ApiApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApiApplication {

public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}

}
20 changes: 20 additions & 0 deletions api/src/main/java/com/example/MovieController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example;

import com.example.response.MovieResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class MovieController {

private final MovieService movieService;

@GetMapping("/movies")
public List<MovieResponse> getMovies() {
return movieService.getMovies();
}
}
4 changes: 4 additions & 0 deletions api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spring:
config:
import:
- application-domain.yml
10 changes: 10 additions & 0 deletions api/src/test/java/com/example/ApiApplicationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ApiApplicationTest {
@Test
void contextLoads() {}
}
3 changes: 3 additions & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation project(":domain")
}
21 changes: 21 additions & 0 deletions application/src/main/java/com/example/MovieService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example;

import com.example.repository.MovieRepository;
import com.example.response.MovieResponse;
import com.example.response.MoviesServiceResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MovieService {

private final MovieRepository movieRepository;

public List<MovieResponse> getMovies() {
return MoviesServiceResponse.of(movieRepository.findMovieWithScreeningAndTheater(Sort.by(Sort.Order.desc("releaseDate"))));
}
}
50 changes: 50 additions & 0 deletions application/src/main/java/com/example/response/MovieResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.response;

import com.example.entity.Movie;
import com.example.entity.MovieTheater;
import com.example.entity.Screening;
import com.example.entity.Theater;
import lombok.Getter;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Getter
public class MovieResponse {
private String title;
private String thumbnailUrl;
private String genre;
private String rating;
private LocalDate releaseDate;
private List<String> theaters;
private List<String> screenings;

MovieResponse(Movie movie) {
this.title = movie.getTitle();
this.thumbnailUrl = movie.getThumbnailUrl();
this.genre = movie.getGenre().getDescription();
this.rating = movie.getRating().getDescription();
this.releaseDate = movie.getReleaseDate();
this.theaters = createTheaters(movie);
this.screenings = createScreening(movie);
}

private static List<String> createTheaters(Movie movie) {
return movie.getMovieTheaters().stream()
.map(MovieTheater::getTheater)
.map(Theater::getName)
.collect(Collectors.toList());
}

private static List<String> createScreening(Movie movie) {
return movie.getScreenings().stream()
.sorted(Comparator.comparing(Screening::getStartedAt))
.map(screening -> screening.getStartedAt().format(DateTimeFormatter.ofPattern("HH:mm"))
+ " ~ "
+ screening.getEndedAt().format(DateTimeFormatter.ofPattern("HH:mm")))
.collect(Collectors.toList());
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 래퍼클래스라고 보이는데 특별하게 여기서 변환하는 이유가 있을까요?

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.response;

import com.example.entity.Movie;
import lombok.Getter;

import java.util.List;

@Getter
public class MoviesServiceResponse {
public static List<MovieResponse> of(List<Movie> movies) {
Copy link

@soonhankwon soonhankwon Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 변수 a, forEach 와 같은 부분은 제거하고 바로 stream을 통해서 리턴해주는게 가독성 면에서 좋아보입니다 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

급하게 개발하다보니까 나중에 수정한다는걸 깜빡했습니다
stream을 이용해서 코드를 개선했습니다

return movies.stream()
.map(MovieResponse::new)
.toList();
}
}
4 changes: 4 additions & 0 deletions application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
spring:
config:
import:
- application-domain.yml
11 changes: 11 additions & 0 deletions application/src/test/java/com/example/ApplicationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class ApplicationTest {

@Test
void contextLoad() {}
}
45 changes: 45 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
}

bootJar.enabled = false

subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'

repositories {
mavenCentral()
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
}
}
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
my-db:
container_name: theater_mysql
image: mysql
environment:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timezone, Charset 에 대한 부분이 없어서 조금 아쉽습니다. 현업에서 많은 버그를 만들어내는 부분이기도 해서요 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mysql 초기설정시 Timezone, Charset 부분을 알아보고 중요성을 알게되었습니다
docker-compose.yml에 설정추가했습니다!

MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: theater
TZ: Asia/Seoul
CHARACTER_SET_SERVER: utf8mb4
ports:
- 3306:3306
volumes:
- ./init:/docker-entrypoint-initdb.d/
restart: always
5 changes: 5 additions & 0 deletions domain/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dependencies {
api 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
}
20 changes: 20 additions & 0 deletions domain/src/main/java/com/example/entity/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;
}
15 changes: 15 additions & 0 deletions domain/src/main/java/com/example/entity/Genre.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.entity;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Genre {
ACTION("액션"),
SF("SF"),
ROMANCE("로멘스"),
HORROR("호로");

private final String description;
}
Loading
Loading