-
Notifications
You must be signed in to change notification settings - Fork 37
[1주차] 영화 서비스 초기 구성 및 상영 중인 영화 조회 API 개발 #2
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 17 commits
3365700
05e045c
cf57e7e
73f4657
182bb6c
ec6816c
91ebafe
4bb0c93
ee32bb4
31dbfd5
11f31b3
aec1f8b
56ae71b
a5e8c91
114abc3
64cc0b8
c1bbe56
3db3353
5633132
d2313e7
c817a13
e89180b
f610fbd
607b68a
2cfc25f
7cfd130
4e0495d
a39f67d
92a415d
2e37685
83c2d2d
7486542
ae8fa2c
4c82c1c
ec4c4e6
3d5dc75
2aa1649
74f6bcf
5c796ae
00d8bf4
b700933
2f7743a
1915964
6f8b81e
0a8fea6
ab3bcbb
1b11e97
fc78548
5e94eb3
f1548af
ebade15
9edcaab
d994a71
4780336
12de380
7a62b2c
eb840f9
14ce595
74f84fa
620e443
44841c1
0103907
2fd597c
ed9e3f8
3aba743
e5c4f91
f6068e8
bf10709
47f4f76
11a7b32
4aec07b
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,9 @@ | ||
| # Spring Boot | ||
| *.log | ||
| *.class | ||
|
|
||
| # Gradle | ||
| .gradle/ | ||
|
|
||
| # IntelliJ IDEA | ||
| .idea/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,51 @@ | ||
| ## [본 과정] 이커머스 핵심 프로세스 구현 | ||
| [단기 스킬업 Redis 교육 과정](https://hh-skillup.oopy.io/) 을 통해 상품 조회 및 주문 과정을 구현하며 현업에서 발생하는 문제를 Redis의 핵심 기술을 통해 해결합니다. | ||
| > Indexing, Caching을 통한 성능 개선 / 단계별 락 구현을 통한 동시성 이슈 해결 (낙관적/비관적 락, 분산락 등) | ||
| # [본 과정] 이커머스 핵심 프로세스 구현 | ||
|
|
||
| ## How to use | ||
|
|
||
| ```bash | ||
| docker compose up -d | ||
| ``` | ||
| ```bash | ||
| curl -X GET http://localhost:8080/api/movies?showDate=2022-05-18 | ||
| curl -X GET http://localhost:8080/api/movies?showDate=2022-05-17 | ||
| ``` | ||
|
|
||
| ## Multi Module | ||
| 총 3개 모듈로 구성되있습니다. | ||
|
|
||
| ### 1. movie-api | ||
| > 영화 도메인을 담당하고 Presentation Layer를 담당하는 모듈입니다. | ||
| - GET /api/movies?showDate=2025-01-01 | ||
|
|
||
| ```json | ||
| // 응답 예시 | ||
| [ | ||
| { | ||
| "id": 0, | ||
| "title": "나 홀로 집에", | ||
| "description": "...", | ||
| "rating": "전체관람가", | ||
| "genre": "코미디", | ||
| "thumbnail": "https://...", | ||
| "runningTime": 103, | ||
| "releaseDate": "1991-07-06", | ||
| "theaters": ["강남점", "안양점"], | ||
| "showtimes": ["08:00 ~ 09:45", "10:00 ~ 11:45"] | ||
| } | ||
| ] | ||
| ``` | ||
| ### 2. core | ||
| > 공통으로 사용하는 Entity, DTO를 담당하는 모듈입니다. | ||
| ### 3. infrastructure | ||
| > DB 인터페이스를 담당하는 모듈입니다. | ||
| > MySQL, Redis 연결을 담당하고 데이터 입출력 로직을 포함합니다. | ||
| > DB가 변경되어도 api, core 모듈의 코드는 최소로 변경합니다. | ||
|
|
||
| ## Architecture | ||
| > 다른 도메인 확장성을 고려한 설계입니다. | ||
| > 모든 api 모듈에서는 Entity -> DTO로 변환하여 리턴합니다. | ||
|
|
||
|  | ||
|
|
||
| ## Table Design | ||
|  |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| ../.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 |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| plugins { | ||
| id 'java' | ||
| id 'org.springframework.boot' version '3.3.2' | ||
| id 'io.spring.dependency-management' version '1.1.6' | ||
| } | ||
|
|
||
| group = 'com.example' | ||
| version = '0.0.1-SNAPSHOT' | ||
|
|
||
| java { | ||
| toolchain { | ||
| languageVersion = JavaLanguageVersion.of(21) | ||
| } | ||
| } | ||
|
|
||
| repositories { | ||
| mavenCentral() | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
| implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' | ||
|
|
||
| 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() | ||
| } |
|
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. System Properties를 BaseEntity로 정의해서 사용을 잘하셨습니다!
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. Getter 어노테이션으로 변경했고 의견주신대로 abstract으로 선언하였습니다! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.example.app.common; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.EntityListeners; | ||
| import jakarta.persistence.MappedSuperclass; | ||
| import lombok.Data; | ||
| import org.springframework.data.annotation.CreatedDate; | ||
| import org.springframework.data.annotation.LastModifiedDate; | ||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Data | ||
| @MappedSuperclass | ||
| @EntityListeners(AuditingEntityListener.class) | ||
| public class BaseEntity { | ||
|
|
||
| @LastModifiedDate | ||
| @Column(insertable = false) | ||
| private LocalDateTime updatedAt; | ||
|
|
||
| @CreatedDate | ||
| @Column(updatable = false) | ||
| private LocalDateTime createdAt; | ||
| } |
|
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.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| package com.example.app.common; | ||
|
|
||
| public record ErrorMessage(String errorCode, String errorMessage) { | ||
| } |
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.example.app.movie.dto; | ||
|
|
||
| import com.example.app.movie.entity.Movie; | ||
| import com.example.app.movie.entity.Showtime; | ||
| import com.example.app.movie.entity.Theater; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Comparator; | ||
| import java.util.List; | ||
|
|
||
| @Builder | ||
| @Getter | ||
| @AllArgsConstructor | ||
| public class MovieDto { | ||
|
|
||
| private Long id; | ||
| private String title; | ||
| private String description; | ||
| private String rating; | ||
| private String genre; | ||
| private String thumbnail; | ||
| private int runningTime; | ||
| private LocalDate releaseDate; | ||
| private List<String> theaters; | ||
| private List<String> showtimes; | ||
|
|
||
| public static MovieDto toDto(Movie movie) { | ||
| var showtimes = movie.getShowtimes() | ||
| .stream() | ||
| .sorted(Comparator.comparing(Showtime::getStart)) | ||
| .map(showtime -> String.format("%s ~ %s", showtime.getStart(), showtime.getEnd())) | ||
| .toList(); | ||
|
|
||
| var theaters = movie.getTheaters() | ||
| .stream() | ||
| .map(Theater::getName) | ||
| .toList(); | ||
|
|
||
| return MovieDto.builder() | ||
| .id(movie.getId()) | ||
| .title(movie.getTitle()) | ||
| .description(movie.getDescription()) | ||
| .rating(movie.getRating().getDescription()) | ||
| .genre(movie.getGenre().getDescription()) | ||
| .thumbnail(movie.getThumbnail()) | ||
| .runningTime(movie.getRunningTime()) | ||
| .releaseDate(movie.getReleaseDate()) | ||
| .theaters(theaters) | ||
| .showtimes(showtimes) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.example.app.movie.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDate; | ||
|
|
||
| public record SearchMovies ( | ||
| @NotNull(message = "상영 날짜는 필수에요") | ||
| LocalDate showDate | ||
| ){ | ||
| } |
|
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. API 요구사항에 맞게 Movie 테이블을 설계하고 엔티티를 만들어주셨습니다 👍
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.
OOP 관점에서 말씀해주시는게 좋은거 같습니다.
일단 N:N 경우 중간테이블 Entity로 만들었습니다. 이걸 어떻게 활용해볼지 고민해보겠습니다.
네 맞습니다. 사실 해당 문제 때문에 실무에서도 Entity만 만들고 모든 연관 관계는 querydsl로 해결하고 있습니다.
👍 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.example.app.movie.entity; | ||
|
|
||
| import com.example.app.common.BaseEntity; | ||
| import com.example.app.movie.type.MovieGenre; | ||
| import com.example.app.movie.type.MovieRating; | ||
| import com.example.app.movie.type.MovieStatus; | ||
| import jakarta.persistence.*; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Entity | ||
| @Table(name="tb_movie") | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class Movie extends BaseEntity { | ||
|
|
||
| @Id | ||
| @Column(name = "movie_id") | ||
| @GeneratedValue(strategy = GenerationType.AUTO) | ||
| private Long id; | ||
|
|
||
| private String title; | ||
|
|
||
| private String description; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| private MovieStatus status; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| private MovieRating rating; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| private MovieGenre genre; | ||
|
|
||
| private String thumbnail; | ||
|
|
||
| @Column(name = "running_time") | ||
| private int runningTime; | ||
|
|
||
| @Column(name = "release_date") | ||
| private LocalDate releaseDate; | ||
|
|
||
| @OneToMany | ||
| @JoinTable( | ||
| name = "tb_movie_showtime", | ||
| joinColumns = @JoinColumn(name = "movie_id"), | ||
| inverseJoinColumns = @JoinColumn(name = "showtime_id")) | ||
| private List<Showtime> showtimes = new ArrayList<>(); | ||
|
|
||
| @ManyToMany | ||
| @JoinTable( | ||
| name = "tb_movie_theater_rel", | ||
| joinColumns = @JoinColumn(name = "movie_id"), | ||
| inverseJoinColumns = @JoinColumn(name = "theater_id")) | ||
| private List<Theater> theaters = new ArrayList<>(); | ||
| } |
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.example.app.movie.entity; | ||
|
|
||
| import com.example.app.common.BaseEntity; | ||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Table(name="tb_movie_showtime") | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class Showtime extends BaseEntity { | ||
|
|
||
| @Id | ||
| @Column(name = "showtime_id") | ||
| @GeneratedValue(strategy = GenerationType.AUTO) | ||
| private Long id; | ||
|
|
||
| @Column(name = "movie_id") | ||
| private Long movieId; | ||
|
|
||
| private String start; | ||
|
|
||
| private String end; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.example.app.movie.entity; | ||
|
|
||
| import com.example.app.common.BaseEntity; | ||
| import jakarta.persistence.*; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Entity | ||
| @Table(name="tb_theater") | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class Theater extends BaseEntity { | ||
|
|
||
| @Id | ||
| @Column(name = "theater_id") | ||
| @GeneratedValue(strategy = GenerationType.AUTO) | ||
| private Long id; | ||
|
|
||
| private String name; | ||
|
|
||
| @ManyToMany(mappedBy = "theaters") | ||
| private List<Movie> movies = new ArrayList<>(); | ||
| } |
|
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. ENUM으로 접근하신 것은 좋은 선택이라고 생각합니다. 👍
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.example.app.movie.type; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum MovieGenre { | ||
| ACTION("액션"), | ||
| COMEDY("코미디"), | ||
| FAMILY("가족"), | ||
| ROMANCE("로맨스"), | ||
| HORROR("호러"), | ||
| SF("SF"); | ||
|
|
||
| private final String description; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.example.app.movie.type; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum MovieRating { | ||
| ALL_AGES("전체관람가"), | ||
| TWELVE_ABOVE("12세 이상 관람가"), | ||
| FIFTEEN_ABOVE("15세 이상 관람가"), | ||
| NO_MINORS("청소년 관람불가"), | ||
| RESTRICTED("제한상영가"); | ||
|
|
||
| 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.
현재 core 모듈과 다른 모듈에 공통적인 dependency (e.g jpa)가 있습니다.
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.
아래처럼 수정했습니다! allprojects, subprojects 차이점이 뭘까요? 둘 다 모든 프로젝트에 영향을 주는 설정 같아서요.
root / build.gradle
movie-api / build.gradle
core / build.gradle
infrastructure / build.gradle
Uh oh!
There was an error while loading. Please reload this page.
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.
루트에서 application을 구동시키는 경우도 있고, 다른 상황도 있을 수 있어 이 경우에 allprojects도 사용됩니다.
현재 찬엽님 프로젝트에서는 root 모듈의 subprojects들에 모든 모듈이 포함됨으로 subprojects만 사용해도 관리 가능합니다 :)