-
Notifications
You must be signed in to change notification settings - Fork 37
[1주차] 멀티모듈 구성 및 영화목록 조회 API 개발 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ffd4d0b
b759791
365c52b
fce079c
9483f12
cadcfd1
780c7ac
875efbd
a0b121d
a998e54
660133c
8637a78
cfb8028
ce38e4a
c4f635c
96b8aae
9e8de25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| /gradlew text eol=lf | ||
| *.bat text eol=crlf | ||
| *.jar binary |
| 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/ |
| 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 및 데이터베이스 접근 | ||
| - 도메인 규칙을 보장하는 로직 포함 | ||
| - **의존성** | ||
| - 독립적이며 다른 모듈에 의존하지 않음 | ||
|
|
||
| --- | ||
|
|
||
| ## 테이블 설계 | ||
| - **도메인** | ||
|  | ||
| - **테이블** | ||
|  | ||
|
|
||
| Movie(영화)와 Theater(극장)은 M:N 관계이므로 MovieTheater로 일대다 다대일 관계로 설계하였습니다. | ||
| Movie(영화)와 Screening(상영)은 1:N 관계이므로 일대다 관계로 설계하였습니다. | ||
| Screening(상영)과 Seat(좌석)은 1:N 관계이므로 일대다 관계로 설계하였습니다. |
| 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' | ||
| } |
| 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); | ||
| } | ||
|
|
||
| } |
| 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| spring: | ||
| config: | ||
| import: | ||
| - application-domain.yml |
| 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() {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| dependencies { | ||
| implementation project(":domain") | ||
| } |
| 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")))); | ||
| } | ||
| } |
| 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()); | ||
| } | ||
| } |
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요한 변수 a, forEach 와 같은 부분은 제거하고 바로 stream을 통해서 리턴해주는게 가독성 면에서 좋아보입니다 :)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 급하게 개발하다보니까 나중에 수정한다는걸 깜빡했습니다 |
||
| return movies.stream() | ||
| .map(MovieResponse::new) | ||
| .toList(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| spring: | ||
| config: | ||
| import: | ||
| - application-domain.yml |
| 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() {} | ||
| } |
| 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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| services: | ||
| my-db: | ||
| container_name: theater_mysql | ||
| image: mysql | ||
| environment: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Timezone, Charset 에 대한 부분이 없어서 조금 아쉽습니다. 현업에서 많은 버그를 만들어내는 부분이기도 해서요 :)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mysql 초기설정시 Timezone, Charset 부분을 알아보고 중요성을 알게되었습니다 |
||
| 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 | ||
| 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' | ||
| } |
| 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; | ||
| } |
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
불필요한 래퍼클래스라고 보이는데 특별하게 여기서 변환하는 이유가 있을까요?