From 2de83069c68b703cf619aec70b32011aca05e5ca Mon Sep 17 00:00:00 2001 From: YEON <65826145+yyy96@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:38:55 +0900 Subject: [PATCH 01/12] Update README.md --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 48216e3..8c5ad31 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,52 @@ -# 1주차 ----- +## Weekly Project Study -## 줌근마켓 : 동네 직거래 시스템 구현 ( 2.24 ~ 3.8 ) [ Feat. 라영지 ] +🚩 **매주 학습할 주제를 선정하며, 해당 주제를 학습하고 간단한 상황 구현을 통해 고민해봅니다.** -Special Thanks to : Pir +🙋🏻‍♀️ 주차별 프로젝트의 코드 리뷰까지 모두 마친 후, README가 업데이트 됩니다. -### 요구사항 +
-1. 동네 정보, 상품 정보, 직거래 시간, 상품 거래 목록을 데이터 베이스에 저장합니다. -2. 사용자는 동네와 직거래 시간을 선택 후 상품을 등록할 수 있습니다. -3. 사용자는 원하는 상품을 선택하여 구매할 수 있습니다. 상품거래가 완료되는 동안 다른 사용자는 해당 상품을 구매할 수 없습니다. -4. 사용자는 동일한 직거래 시간에 여러 상품을 구매할 수 없습니다. -5. 사용자는 자신이 거래 또는 판매한 상품 목록을 조회할 수 있습니다. +### 🗓 1주차 (2.24 ~ 3.8) -### 구현해야 할 기능 리스트 +**✏️ 학습 목표** +1. JPA 연관관계에 대해 다시 한번 학습 +2. 동적 쿼리가 필요한 경우를 생각해보고, QueryDSL을 통해 작성 -- 구매 또는 판매한 상품 조회 -- 전체 판매되고있는 상품 조회 -- 상품 구매 - 이미 동일한 거래 시간에 상품 구매 예정이 되어있다면 다른 상품은 구매가 불가 -- 상품 판매 - 상품을 등록, 수정, 삭제 할 수 있음 +**주제** +- 줌근마켓 : 동네 직거래 시스템 구현 + +**요구사항 (상황구현)** +- 동네 정보, 상품 정보, 직거래 시간, 상품 거래 목록을 데이터 베이스에 저장합니다. +- 사용자는 동네와 직거래 시간을 선택 후 상품을 등록할 수 있습니다. +- 사용자는 원하는 상품을 선택하여 구매할 수 있습니다. 상품거래가 완료되는 동안 다른 사용자는 해당 상품을 구매할 수 없습니다. +- 사용자는 동일한 직거래 시간에 여러 상품을 구매할 수 없습니다. +- 사용자는 자신이 거래 또는 판매한 상품 목록을 조회할 수 있습니다. + +**참고** +- [구현된 프로젝트 브랜치 → **querydsl**]() +- [해당 프로젝트 팀원 코드리뷰](https://github.com/zum-spring-study/Daily-Project/pull/2) + +
+ +### 🗓 2주차 (3.10 ~ 3.22) + +**✏️ 학습 목표** +1. 실제로 동시성이 발생하는 경우를 생각해보고, 해당 동시성을 예방하기 위한 적절한 방법들을 모색 +3. 여러 방법들을 비교하고 적절한 대안을 생각 + +**주제** +- 줌터파켓: 한국에 드디어 상륙한 캣츠! 티켓 1000장을 잡아라! + +**요구사항 (상황구현)** +- 하나의 예매 서비스에 다량의 유저들이 접속한다는걸 가정합니다. +- 유저들은 접속하는 환경이 다를겁니다 ( ex. 모바일 , 컴퓨터 ) 하나의 서버가 아닌 여러 서버를 가정합니다. +- 뮤지컬의 경우 (날짜는 무관) 3일동안 1000장의 티켓이 발행됩니다. +- 1000장의 수량이 떨어질 경우 1001번째 사람은 티켓을 구매할 수 없습니다. +- 티켓을 이미 구매한 사람은 사제기의 요소를 방지하기 위해 더이상의 티켓을 구매할 순 없습니다. +- 티켓을 구매했을 경우 당일 뿐만이 아닌 다른날도 구매가 불가합니다. + +**참고** +- [구현된 프로젝트 브랜치 → **concurrency**]() +- [해당 프로젝트 팀원 코드리뷰]() + +

From 3302658658828f60673f37be5bef89e5845f5942 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Mon, 20 Mar 2023 00:17:43 +0900 Subject: [PATCH 02/12] setting: update setting --- ...plication.java => WeeklyStudyProject.java} | 4 ++-- src/main/resources/application.yml | 19 +++++++++++++++++++ ...ests.java => WeeklyStudyProjectTests.java} | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) rename src/main/java/com/week/zumgnmarket/{ZumgnmarketApplication.java => WeeklyStudyProject.java} (68%) rename src/test/java/com/week/zumgnmarket/{ZumgnmarketApplicationTests.java => WeeklyStudyProjectTests.java} (84%) diff --git a/src/main/java/com/week/zumgnmarket/ZumgnmarketApplication.java b/src/main/java/com/week/zumgnmarket/WeeklyStudyProject.java similarity index 68% rename from src/main/java/com/week/zumgnmarket/ZumgnmarketApplication.java rename to src/main/java/com/week/zumgnmarket/WeeklyStudyProject.java index 87f7f1c..1ac5271 100644 --- a/src/main/java/com/week/zumgnmarket/ZumgnmarketApplication.java +++ b/src/main/java/com/week/zumgnmarket/WeeklyStudyProject.java @@ -4,10 +4,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class ZumgnmarketApplication { +public class WeeklyStudyProject { public static void main(String[] args) { - SpringApplication.run(ZumgnmarketApplication.class, args); + SpringApplication.run(WeeklyStudyProject.class, args); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e69de29..c61453e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -0,0 +1,19 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/study?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimeZone=Asia/Seoul + username: study + password: study123 + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: true + +logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace \ No newline at end of file diff --git a/src/test/java/com/week/zumgnmarket/ZumgnmarketApplicationTests.java b/src/test/java/com/week/zumgnmarket/WeeklyStudyProjectTests.java similarity index 84% rename from src/test/java/com/week/zumgnmarket/ZumgnmarketApplicationTests.java rename to src/test/java/com/week/zumgnmarket/WeeklyStudyProjectTests.java index 2495be3..3d03c13 100644 --- a/src/test/java/com/week/zumgnmarket/ZumgnmarketApplicationTests.java +++ b/src/test/java/com/week/zumgnmarket/WeeklyStudyProjectTests.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class ZumgnmarketApplicationTests { +class WeeklyStudyProjectTests { @Test void contextLoads() { From 792215bfc10ba51e6cad0eb941f92467344af905 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Mon, 20 Mar 2023 23:50:59 +0900 Subject: [PATCH 03/12] setting: update setting --- .../zumgnmarket/common/config/JpaConfig.java | 9 +++++++++ .../common/config/QuerydslConfig.java | 20 +++++++++++++++++++ src/main/resources/application.yml | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/week/zumgnmarket/common/config/JpaConfig.java create mode 100644 src/main/java/com/week/zumgnmarket/common/config/QuerydslConfig.java diff --git a/src/main/java/com/week/zumgnmarket/common/config/JpaConfig.java b/src/main/java/com/week/zumgnmarket/common/config/JpaConfig.java new file mode 100644 index 0000000..dc1ca51 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/common/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.week.zumgnmarket.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaConfig { +} diff --git a/src/main/java/com/week/zumgnmarket/common/config/QuerydslConfig.java b/src/main/java/com/week/zumgnmarket/common/config/QuerydslConfig.java new file mode 100644 index 0000000..17ebfab --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/common/config/QuerydslConfig.java @@ -0,0 +1,20 @@ +package com.week.zumgnmarket.common.config; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +@Configuration +public class QuerydslConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c61453e..153d1a3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ spring: datasource: url: jdbc:mysql://localhost:3306/study?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimeZone=Asia/Seoul username: study - password: study123 + password: study123!@# driver-class-name: com.mysql.cj.jdbc.Driver jpa: From 010ae3ba5aace3a27e0d3a49e87f76deb99b5661 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Mon, 20 Mar 2023 23:51:25 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zumgnmarket/common/domain/BaseEntity.java | 23 +++++++++ .../week/zumgnmarket/order/entity/Order.java | 50 +++++++++++++++++++ .../order/entity/OrderRepository.java | 6 +++ .../week/zumgnmarket/order/entity/Orders.java | 18 +++++++ .../zumgnmarket/ticket/entity/Ticket.java | 44 ++++++++++++++++ .../ticket/entity/TicketRepository.java | 6 +++ .../week/zumgnmarket/user/entity/User.java | 33 ++++++++++++ .../zumgnmarket/user/entity/UserLogin.java | 27 ++++++++++ .../user/entity/UserRepository.java | 6 +++ .../order/entity/OrderRepositoryTest.java | 47 +++++++++++++++++ 10 files changed, 260 insertions(+) create mode 100644 src/main/java/com/week/zumgnmarket/common/domain/BaseEntity.java create mode 100644 src/main/java/com/week/zumgnmarket/order/entity/Order.java create mode 100644 src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java create mode 100644 src/main/java/com/week/zumgnmarket/order/entity/Orders.java create mode 100644 src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java create mode 100644 src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java create mode 100644 src/main/java/com/week/zumgnmarket/user/entity/User.java create mode 100644 src/main/java/com/week/zumgnmarket/user/entity/UserLogin.java create mode 100644 src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java create mode 100644 src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java diff --git a/src/main/java/com/week/zumgnmarket/common/domain/BaseEntity.java b/src/main/java/com/week/zumgnmarket/common/domain/BaseEntity.java new file mode 100644 index 0000000..1a10e52 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/common/domain/BaseEntity.java @@ -0,0 +1,23 @@ +package com.week.zumgnmarket.common.domain; +import java.time.LocalDateTime; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime updatedDate; + +} \ No newline at end of file diff --git a/src/main/java/com/week/zumgnmarket/order/entity/Order.java b/src/main/java/com/week/zumgnmarket/order/entity/Order.java new file mode 100644 index 0000000..903ce62 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/entity/Order.java @@ -0,0 +1,50 @@ +package com.week.zumgnmarket.order.entity; + +import static javax.persistence.FetchType.*; + +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import com.week.zumgnmarket.common.domain.BaseEntity; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "orders") +public class Order extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "ticket_id") + private Ticket ticket; + + private int ticketCount; + + @Builder + public Order(User user, Ticket ticket, int ticketCount) { + this.user = user; + this.ticket = ticket; + this.ticketCount = ticketCount; + } +} diff --git a/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java b/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java new file mode 100644 index 0000000..8c7b95a --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java @@ -0,0 +1,6 @@ +package com.week.zumgnmarket.order.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderRepository extends JpaRepository { +} diff --git a/src/main/java/com/week/zumgnmarket/order/entity/Orders.java b/src/main/java/com/week/zumgnmarket/order/entity/Orders.java new file mode 100644 index 0000000..3d84316 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/entity/Orders.java @@ -0,0 +1,18 @@ +package com.week.zumgnmarket.order.entity; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.CascadeType; +import javax.persistence.Embeddable; +import javax.persistence.OneToMany; + +import lombok.Getter; + +@Getter +@Embeddable +public class Orders { + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + List orders = new ArrayList<>(); + +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java new file mode 100644 index 0000000..be2840c --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java @@ -0,0 +1,44 @@ +package com.week.zumgnmarket.ticket.entity; + +import static javax.persistence.FetchType.*; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import com.week.zumgnmarket.common.domain.BaseEntity; +import com.week.zumgnmarket.order.entity.Order; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "tickets") +public class Ticket extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private int price; + + private int remainingCount; + + @Builder + public Ticket(String name, int price, int remainingCount) { + this.name = name; + this.price = price; + this.remainingCount = remainingCount; + } +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java new file mode 100644 index 0000000..4ea86d6 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java @@ -0,0 +1,6 @@ +package com.week.zumgnmarket.ticket.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TicketRepository extends JpaRepository { +} diff --git a/src/main/java/com/week/zumgnmarket/user/entity/User.java b/src/main/java/com/week/zumgnmarket/user/entity/User.java new file mode 100644 index 0000000..40b02f7 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/user/entity/User.java @@ -0,0 +1,33 @@ +package com.week.zumgnmarket.user.entity; + +import javax.persistence.*; + +import com.week.zumgnmarket.common.domain.BaseEntity; +import com.week.zumgnmarket.order.entity.Orders; + +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "users") +public class User extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Embedded + private UserLogin userLogin; + + @Embedded + private Orders orders; + + @Builder + public User(String name, String email, String password) { + this.name = name; + this.userLogin = new UserLogin(email, password); + } + +} \ No newline at end of file diff --git a/src/main/java/com/week/zumgnmarket/user/entity/UserLogin.java b/src/main/java/com/week/zumgnmarket/user/entity/UserLogin.java new file mode 100644 index 0000000..53811a2 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/user/entity/UserLogin.java @@ -0,0 +1,27 @@ +package com.week.zumgnmarket.user.entity; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor +public class UserLogin { + + @Column(length = 50, nullable = false) + private String email; + + @Column(length = 20, nullable = false) + private String password; + + public UserLogin(String email, String password) { + this.email = email; + this.password = password; + } + +} \ No newline at end of file diff --git a/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java b/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java new file mode 100644 index 0000000..8267446 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java @@ -0,0 +1,6 @@ +package com.week.zumgnmarket.user.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java b/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java new file mode 100644 index 0000000..9a146b4 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java @@ -0,0 +1,47 @@ +package com.week.zumgnmarket.order.entity; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@DataJpaTest +public class OrderRepositoryTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private OrderRepository orderRepository; + + User 회원; + Ticket 티켓; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @Test + void 연관관계_매핑() { + //given + Order 주문 = new Order(회원, 티켓, 1); + + //when + Order result = orderRepository.save(주문); + + //then + assertEquals(result.getTicket(), 티켓); + } +} From 428f6a41e5b4422b717410cf756b01e9d4969cec Mon Sep 17 00:00:00 2001 From: yyy96 Date: Wed, 22 Mar 2023 20:13:24 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EA=B8=B0=EB=B3=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderController.java | 26 ++++++ .../order/controller/OrderFacade.java | 31 +++++++ .../zumgnmarket/order/dto/OrderRequest.java | 11 +++ .../zumgnmarket/order/dto/OrderResponse.java | 33 ++++++++ .../week/zumgnmarket/order/entity/Order.java | 10 ++- .../order/entity/OrderRepository.java | 2 +- .../week/zumgnmarket/order/entity/Orders.java | 2 +- .../order/service/OrderService.java | 9 +++ .../order/service/OrderServiceImpl.java | 25 ++++++ .../zumgnmarket/ticket/entity/Ticket.java | 24 +++--- .../ticket/entity/TicketRepository.java | 2 +- .../ticket/service/TicketService.java | 22 +++++ .../user/entity/UserRepository.java | 2 +- .../zumgnmarket/user/service/UserService.java | 22 +++++ .../order/entity/OrderRepositoryTest.java | 2 +- .../order/service/OrderServiceTest.java | 80 +++++++++++++++++++ 16 files changed, 287 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/order/controller/OrderController.java create mode 100644 src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java create mode 100644 src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java create mode 100644 src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java create mode 100644 src/main/java/com/week/zumgnmarket/order/service/OrderService.java create mode 100644 src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java create mode 100644 src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java create mode 100644 src/main/java/com/week/zumgnmarket/user/service/UserService.java create mode 100644 src/test/java/com/week/zumgnmarket/order/service/OrderServiceTest.java diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderController.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderController.java new file mode 100644 index 0000000..b11638a --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderController.java @@ -0,0 +1,26 @@ +package com.week.zumgnmarket.order.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.week.zumgnmarket.order.dto.OrderRequest; +import com.week.zumgnmarket.order.dto.OrderResponse; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/orders") +public class OrderController { + + private final OrderFacade orderFacade; + + @PostMapping("/tickets") + public ResponseEntity orderTickets(@RequestBody OrderRequest request) { + return ResponseEntity.ok(orderFacade.order(request)); + } + +} diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java new file mode 100644 index 0000000..73d695a --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java @@ -0,0 +1,31 @@ +package com.week.zumgnmarket.order.controller; + +import org.springframework.stereotype.Service; + +import com.week.zumgnmarket.order.dto.OrderRequest; +import com.week.zumgnmarket.order.dto.OrderResponse; +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.order.service.OrderService; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.service.TicketService; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.service.UserService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OrderFacade { + + private final UserService userService; + private final TicketService ticketService; + private final OrderService orderService; + + public OrderResponse order(OrderRequest request) { + User user = userService.getUser(request.getUserId()); + Ticket ticket = ticketService.getTicket(request.getTicketId()); + Order order = orderService.orderTicket(user, ticket, request.getQuantity()); + + return OrderResponse.of(order, user, ticket); + } +} diff --git a/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java b/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java new file mode 100644 index 0000000..38ebd0b --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java @@ -0,0 +1,11 @@ +package com.week.zumgnmarket.order.dto; + +import lombok.Getter; + +@Getter +public class OrderRequest { + public Long userId; + public Long ticketId; + public int quantity; + +} diff --git a/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java b/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java new file mode 100644 index 0000000..003a599 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java @@ -0,0 +1,33 @@ +package com.week.zumgnmarket.order.dto; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class OrderResponse { + public Long orderId; + public Long userId; + public Long ticketId; + public int quantity; + + @Builder + public OrderResponse(Long orderId, Long userId, Long ticketId, int quantity) { + this.orderId = orderId; + this.userId = userId; + this.ticketId = ticketId; + this.quantity = quantity; + } + + public static OrderResponse of(Order order, User user, Ticket ticket) { + return OrderResponse.builder() + .orderId(order.getId()) + .userId(user.getId()) + .ticketId(ticket.getId()) + .quantity(order.getTicketCount()) + .build(); + } +} diff --git a/src/main/java/com/week/zumgnmarket/order/entity/Order.java b/src/main/java/com/week/zumgnmarket/order/entity/Order.java index 903ce62..88924eb 100644 --- a/src/main/java/com/week/zumgnmarket/order/entity/Order.java +++ b/src/main/java/com/week/zumgnmarket/order/entity/Order.java @@ -42,9 +42,17 @@ public class Order extends BaseEntity { private int ticketCount; @Builder - public Order(User user, Ticket ticket, int ticketCount) { + private Order(User user, Ticket ticket, int ticketCount) { this.user = user; this.ticket = ticket; this.ticketCount = ticketCount; } + + public static Order of(User user, Ticket ticket, int ticketCount) { + return Order.builder() + .user(user) + .ticket(ticket) + .ticketCount(ticketCount) + .build(); + } } diff --git a/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java b/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java index 8c7b95a..c049604 100644 --- a/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java +++ b/src/main/java/com/week/zumgnmarket/order/entity/OrderRepository.java @@ -2,5 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface OrderRepository extends JpaRepository { +public interface OrderRepository extends JpaRepository { } diff --git a/src/main/java/com/week/zumgnmarket/order/entity/Orders.java b/src/main/java/com/week/zumgnmarket/order/entity/Orders.java index 3d84316..d501a89 100644 --- a/src/main/java/com/week/zumgnmarket/order/entity/Orders.java +++ b/src/main/java/com/week/zumgnmarket/order/entity/Orders.java @@ -12,7 +12,7 @@ @Getter @Embeddable public class Orders { - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) List orders = new ArrayList<>(); } diff --git a/src/main/java/com/week/zumgnmarket/order/service/OrderService.java b/src/main/java/com/week/zumgnmarket/order/service/OrderService.java new file mode 100644 index 0000000..e2b16d2 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/service/OrderService.java @@ -0,0 +1,9 @@ +package com.week.zumgnmarket.order.service; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +public interface OrderService { + Order orderTicket(User user, Ticket ticket, int quantity); +} diff --git a/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java b/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java new file mode 100644 index 0000000..a5c2159 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java @@ -0,0 +1,25 @@ +package com.week.zumgnmarket.order.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.order.entity.OrderRepository; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional +@RequiredArgsConstructor +public class OrderServiceImpl implements OrderService { + + private final OrderRepository orderRepository; + + @Override + public Order orderTicket(User user, Ticket ticket, int quantity) { + ticket.decreaseQuantity(quantity); + return orderRepository.save(Order.of(user, ticket, quantity)); + } +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java index be2840c..0871884 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java @@ -1,19 +1,12 @@ package com.week.zumgnmarket.ticket.entity; -import static javax.persistence.FetchType.*; - -import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; import javax.persistence.Table; import com.week.zumgnmarket.common.domain.BaseEntity; -import com.week.zumgnmarket.order.entity.Order; import lombok.AccessLevel; import lombok.Builder; @@ -33,12 +26,23 @@ public class Ticket extends BaseEntity { private int price; - private int remainingCount; + private int quantity; @Builder - public Ticket(String name, int price, int remainingCount) { + public Ticket(String name, int price, int quantity) { this.name = name; this.price = price; - this.remainingCount = remainingCount; + this.quantity = quantity; + } + + public void decreaseQuantity(int orderQuantity) { + checkQuantity(orderQuantity); + this.quantity -= orderQuantity; + } + + private void checkQuantity(int orderQuantity) { + if (quantity == 0 || quantity < orderQuantity) { + throw new RuntimeException(); + } } } diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java index 4ea86d6..94a1a8a 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketRepository.java @@ -2,5 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface TicketRepository extends JpaRepository { +public interface TicketRepository extends JpaRepository { } diff --git a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java new file mode 100644 index 0000000..7ca7c92 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java @@ -0,0 +1,22 @@ +package com.week.zumgnmarket.ticket.service; + +import javax.persistence.EntityNotFoundException; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; + +@Service +@Transactional +public class TicketService { + + private TicketRepository ticketRepository; + + @Transactional(readOnly = true) + public Ticket getTicket(Long id) { + return ticketRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + } +} diff --git a/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java b/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java index 8267446..26429cf 100644 --- a/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java +++ b/src/main/java/com/week/zumgnmarket/user/entity/UserRepository.java @@ -2,5 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { } diff --git a/src/main/java/com/week/zumgnmarket/user/service/UserService.java b/src/main/java/com/week/zumgnmarket/user/service/UserService.java new file mode 100644 index 0000000..431a4ed --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/user/service/UserService.java @@ -0,0 +1,22 @@ +package com.week.zumgnmarket.user.service; + +import javax.persistence.EntityNotFoundException; + +import org.springframework.stereotype.Service; + +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User getUser(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + id)); + } +} diff --git a/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java b/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java index 9a146b4..92073f4 100644 --- a/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java +++ b/src/test/java/com/week/zumgnmarket/order/entity/OrderRepositoryTest.java @@ -36,7 +36,7 @@ void setup() { @Test void 연관관계_매핑() { //given - Order 주문 = new Order(회원, 티켓, 1); + Order 주문 = Order.of(회원, 티켓, 1); //when Order result = orderRepository.save(주문); diff --git a/src/test/java/com/week/zumgnmarket/order/service/OrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/OrderServiceTest.java new file mode 100644 index 0000000..f8dbda2 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/service/OrderServiceTest.java @@ -0,0 +1,80 @@ +package com.week.zumgnmarket.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@SpringBootTest +public class OrderServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private OrderService orderService; + + private User 회원; + private Ticket 티켓; + private final int thread = 1000; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @AfterEach + void after() { + userRepository.deleteAll(); + ticketRepository.deleteAll(); + } + + @Test + void 티켓_주문() { + //given + int orderQuantity = 2; + int remainingQuantity = 티켓.getQuantity(); + + // when + Order 주문 = orderService.orderTicket(회원, 티켓, orderQuantity); + + //then + assertEquals(티켓.getQuantity(), remainingQuantity - orderQuantity); + } + + @Test + void 동시에_티켓_주문_실패() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(123); + CountDownLatch countDownLatch = new CountDownLatch(thread); + + //when + for (int i = 0; i < thread; i++) { + executorService.execute(() -> { + orderService.orderTicket(회원, 티켓, 1); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + //then + assertNotEquals(0, 티켓.getQuantity()); + } +} From f7b264a89acfa86dfe634019ec2457afb26c19bd Mon Sep 17 00:00:00 2001 From: yyy96 Date: Wed, 22 Mar 2023 20:39:00 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20synchronized=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/OrderServiceImpl.java | 2 + .../service/SynchronizedOrderService.java | 23 ++++++ .../service/SynchronizedOrderServiceTest.java | 72 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java create mode 100644 src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java diff --git a/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java b/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java index a5c2159..5106dba 100644 --- a/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java +++ b/src/main/java/com/week/zumgnmarket/order/service/OrderServiceImpl.java @@ -1,5 +1,6 @@ package com.week.zumgnmarket.order.service; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -10,6 +11,7 @@ import lombok.RequiredArgsConstructor; +@Primary @Service @Transactional @RequiredArgsConstructor diff --git a/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java b/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java new file mode 100644 index 0000000..ebc906b --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java @@ -0,0 +1,23 @@ +package com.week.zumgnmarket.order.service; + +import org.springframework.stereotype.Service; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.order.entity.OrderRepository; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class SynchronizedOrderService implements OrderService { + + private final OrderRepository orderRepository; + + @Override + public synchronized Order orderTicket(User user, Ticket ticket, int quantity) { + ticket.decreaseQuantity(quantity); + return orderRepository.save(Order.of(user, ticket, quantity)); + } +} \ No newline at end of file diff --git a/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java new file mode 100644 index 0000000..33edd5f --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java @@ -0,0 +1,72 @@ +package com.week.zumgnmarket.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@SpringBootTest +public class SynchronizedOrderServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private SynchronizedOrderService orderService; + + private User 회원; + private Ticket 티켓; + private final int thread = 1000; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @AfterEach + void after() { + userRepository.deleteAll(); + ticketRepository.deleteAll(); + } + + /** + * 문제) + * 서버가 한 대일 경우 문제가 없지만 + * synchronized 는 각 프로세스 안에서만 보장이 되기 때문에 + * 여러 서버 스레드에서 접근을 하게 된다면 race condition 이 발생할 수 있습니다. + * */ + @Test + void 동시에_티켓_주문() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(123); + CountDownLatch countDownLatch = new CountDownLatch(thread); + + //when + for (int i = 0; i < thread; i++) { + executorService.execute(() -> { + orderService.orderTicket(회원, 티켓, 1); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + //then + assertEquals(0, 티켓.getQuantity()); + } +} From 29ff99d4ba34a7ae7993d2cc62bbc3729e94a543 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Wed, 22 Mar 2023 23:08:42 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=A3=BC=EC=84=9D,exception?= =?UTF-8?q?=20=EB=93=B1=20=EA=B8=B0=ED=83=80=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderFacade.java | 4 ++-- .../service/SynchronizedOrderService.java | 7 +++++++ .../zumgnmarket/ticket/entity/Ticket.java | 3 ++- .../exception/NotEnoughTicketException.java | 21 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/ticket/exception/NotEnoughTicketException.java diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java index 73d695a..4f5e5d7 100644 --- a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java @@ -1,6 +1,6 @@ package com.week.zumgnmarket.order.controller; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; import com.week.zumgnmarket.order.dto.OrderRequest; import com.week.zumgnmarket.order.dto.OrderResponse; @@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor; -@Service +@Component @RequiredArgsConstructor public class OrderFacade { diff --git a/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java b/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java index ebc906b..e00c2a1 100644 --- a/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java +++ b/src/main/java/com/week/zumgnmarket/order/service/SynchronizedOrderService.java @@ -9,6 +9,13 @@ import lombok.RequiredArgsConstructor; +/** + * synchronized + * 문제) 서버가 한 대일 경우 문제가 없지만 + * synchronized 는 각 프로세스 안에서만 보장이 되기 때문에 + * 여러 서버 스레드에서 접근을 하게 된다면 race condition 이 발생할 수 있다. + * */ + @Service @RequiredArgsConstructor public class SynchronizedOrderService implements OrderService { diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java index 0871884..d573dfd 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java @@ -7,6 +7,7 @@ import javax.persistence.Table; import com.week.zumgnmarket.common.domain.BaseEntity; +import com.week.zumgnmarket.ticket.exception.NotEnoughTicketException; import lombok.AccessLevel; import lombok.Builder; @@ -42,7 +43,7 @@ public void decreaseQuantity(int orderQuantity) { private void checkQuantity(int orderQuantity) { if (quantity == 0 || quantity < orderQuantity) { - throw new RuntimeException(); + throw new NotEnoughTicketException(); } } } diff --git a/src/main/java/com/week/zumgnmarket/ticket/exception/NotEnoughTicketException.java b/src/main/java/com/week/zumgnmarket/ticket/exception/NotEnoughTicketException.java new file mode 100644 index 0000000..09445c7 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/exception/NotEnoughTicketException.java @@ -0,0 +1,21 @@ +package com.week.zumgnmarket.ticket.exception; + +public class NotEnoughTicketException extends RuntimeException { + + public NotEnoughTicketException() { + super(); + } + + public NotEnoughTicketException(String message) { + super(message); + } + + public NotEnoughTicketException(String message, Throwable cause) { + super(message, cause); + } + + public NotEnoughTicketException(Throwable cause) { + super(cause); + } + +} From e94afc91313aa8d224513cf9c92b6c4bf53eba2e Mon Sep 17 00:00:00 2001 From: yyy96 Date: Wed, 22 Mar 2023 23:13:30 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20PessimisticLock=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(error)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PessimisticLockOrderService.java | 32 +++++++++ .../entity/TicketQueryDslRepository.java | 29 ++++++++ .../ticket/service/TicketService.java | 14 +++- .../PessimisticLockOrderServiceTest.java | 70 +++++++++++++++++++ .../service/SynchronizedOrderServiceTest.java | 6 -- 5 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/order/service/PessimisticLockOrderService.java create mode 100644 src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java create mode 100644 src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java diff --git a/src/main/java/com/week/zumgnmarket/order/service/PessimisticLockOrderService.java b/src/main/java/com/week/zumgnmarket/order/service/PessimisticLockOrderService.java new file mode 100644 index 0000000..b2b5787 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/service/PessimisticLockOrderService.java @@ -0,0 +1,32 @@ +package com.week.zumgnmarket.order.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.order.entity.OrderRepository; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.RequiredArgsConstructor; + +/** + * Pessimistic Lock + * 실제로 데이터에 Lock 을 걸어서 정합성을 맞추는 방법. + * Lock 이 해제되기 전에는 다른 트랜잭션에서 데이터를 가져갈 수 없다. + * */ + +@Service +@Transactional +@RequiredArgsConstructor +public class PessimisticLockOrderService implements OrderService { + + private final OrderRepository orderRepository; + + @Override + public Order orderTicket(User user, Ticket ticket, int quantity) { + ticket.decreaseQuantity(quantity); + return orderRepository.save(Order.of(user, ticket, quantity)); + } + +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java new file mode 100644 index 0000000..0a23afd --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java @@ -0,0 +1,29 @@ +package com.week.zumgnmarket.ticket.entity; + +import static com.week.zumgnmarket.ticket.entity.QTicket.*; + +import java.util.Optional; + +import javax.persistence.LockModeType; + +import org.springframework.data.jpa.repository.Lock; +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class TicketQueryDslRepository { + + private final JPAQueryFactory queryFactory; + + @Lock(value = LockModeType.PESSIMISTIC_WRITE) + public Optional findByIdWithLock(Long id) { + return Optional.ofNullable(queryFactory.selectFrom(ticket) + .where(ticket.id.eq(id)) + .fetchOne()); + } + +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java index 7ca7c92..b9fb7d9 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java +++ b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java @@ -6,17 +6,29 @@ import org.springframework.transaction.annotation.Transactional; import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketQueryDslRepository; import com.week.zumgnmarket.ticket.entity.TicketRepository; +import lombok.RequiredArgsConstructor; + @Service @Transactional +@RequiredArgsConstructor public class TicketService { - private TicketRepository ticketRepository; + private final TicketRepository ticketRepository; + + private final TicketQueryDslRepository ticketQueryDslRepository; @Transactional(readOnly = true) public Ticket getTicket(Long id) { return ticketRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); } + + @Transactional(readOnly = true) + public Ticket getTicketWithLock(Long id) { + return ticketQueryDslRepository.findByIdWithLock(id) + .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + } } diff --git a/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java new file mode 100644 index 0000000..79d3797 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java @@ -0,0 +1,70 @@ +package com.week.zumgnmarket.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.ticket.service.TicketService; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@SpringBootTest +public class PessimisticLockOrderServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private TicketService ticketService; + @Autowired + private PessimisticLockOrderService orderService; + + private User 회원; + private Ticket 티켓; + private final int thread = 1000; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @AfterEach + void after() { + userRepository.deleteAll(); + ticketRepository.deleteAll(); + } + + @Test + void 동시에_티켓_주문() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(123); + CountDownLatch countDownLatch = new CountDownLatch(thread); + + //when + for (int i = 0; i < thread; i++) { + executorService.execute(() -> { + //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 + orderService.orderTicket(회원, ticketService.getTicketWithLock(티켓.getId()), 1); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + //then + assertEquals(0, 티켓.getQuantity()); + } +} diff --git a/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java index 33edd5f..29206ab 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/SynchronizedOrderServiceTest.java @@ -45,12 +45,6 @@ void after() { ticketRepository.deleteAll(); } - /** - * 문제) - * 서버가 한 대일 경우 문제가 없지만 - * synchronized 는 각 프로세스 안에서만 보장이 되기 때문에 - * 여러 서버 스레드에서 접근을 하게 된다면 race condition 이 발생할 수 있습니다. - * */ @Test void 동시에_티켓_주문() throws InterruptedException { //given From 4e70400c8eeeba99b8e9ddd43a009e17eb958879 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Wed, 22 Mar 2023 23:57:29 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20OptimisticLock=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(error)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderFacade.java | 23 ++++++ .../zumgnmarket/order/dto/OrderRequest.java | 5 ++ .../service/OptimisticLockOrderService.java | 27 +++++++ .../zumgnmarket/ticket/entity/Ticket.java | 7 ++ .../entity/TicketQueryDslRepository.java | 9 ++- .../ticket/service/TicketService.java | 10 ++- .../OptimisticLockOrderServiceTest.java | 73 +++++++++++++++++++ .../PessimisticLockOrderServiceTest.java | 2 +- 8 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/order/service/OptimisticLockOrderService.java create mode 100644 src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java index 4f5e5d7..3096233 100644 --- a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java @@ -28,4 +28,27 @@ public OrderResponse order(OrderRequest request) { return OrderResponse.of(order, user, ticket); } + + public OrderResponse orderWithPLock(OrderRequest request) { + User user = userService.getUser(request.getUserId()); + Ticket ticket = ticketService.getTicketWithPessimisticLock(request.getTicketId()); + Order order = orderService.orderTicket(user, ticket, request.getQuantity()); + + return OrderResponse.of(order, user, ticket); + } + + public OrderResponse orderWithOLock(OrderRequest request) throws InterruptedException { + User user = userService.getUser(request.getUserId()); + + //OptimisticLock 같은 경우 실패했을 때 재시도를 위해 while 을 사용하였습니다. + while (true) { + try { + Ticket ticket = ticketService.getTicketWithOptimisticLock(request.getTicketId()); + Order order = orderService.orderTicket(user, ticket, request.getQuantity()); + return OrderResponse.of(order, user, ticket); + } catch (Exception e) { + Thread.sleep(10); + } + } + } } diff --git a/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java b/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java index 38ebd0b..0089a68 100644 --- a/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java +++ b/src/main/java/com/week/zumgnmarket/order/dto/OrderRequest.java @@ -8,4 +8,9 @@ public class OrderRequest { public Long ticketId; public int quantity; + public OrderRequest(Long userId, Long ticketId, int quantity) { + this.userId = userId; + this.ticketId = ticketId; + this.quantity = quantity; + } } diff --git a/src/main/java/com/week/zumgnmarket/order/service/OptimisticLockOrderService.java b/src/main/java/com/week/zumgnmarket/order/service/OptimisticLockOrderService.java new file mode 100644 index 0000000..7d8f856 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/order/service/OptimisticLockOrderService.java @@ -0,0 +1,27 @@ +package com.week.zumgnmarket.order.service; + +import org.springframework.stereotype.Service; + +import com.week.zumgnmarket.order.entity.Order; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.user.entity.User; + +import lombok.RequiredArgsConstructor; + +/** + * Optimistic Lock + * Lock 이 아니라 Version 을 통해 정합성을 맞추는 방법. + * 처음에 현재 데이터의 version 을 읽고, + * 이후 update 하는 시점에서 처음 읽은 버전과 동일한지 확인 후 update 수행. + * */ + +@Service +@RequiredArgsConstructor +public class OptimisticLockOrderService implements OrderService{ + + @Override + public Order orderTicket(User user, Ticket ticket, int quantity) { + ticket.decreaseQuantity(quantity); + return Order.of(user, ticket, quantity); + } +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java index d573dfd..5a03eda 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/Ticket.java @@ -5,18 +5,21 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; +import javax.persistence.Version; import com.week.zumgnmarket.common.domain.BaseEntity; import com.week.zumgnmarket.ticket.exception.NotEnoughTicketException; import lombok.AccessLevel; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode @Table(name = "tickets") public class Ticket extends BaseEntity { @Id @@ -29,6 +32,10 @@ public class Ticket extends BaseEntity { private int quantity; + //Optimistic Lock Version + @Version + private Long version; + @Builder public Ticket(String name, int price, int quantity) { this.name = name; diff --git a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java index 0a23afd..d9b8ac2 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java +++ b/src/main/java/com/week/zumgnmarket/ticket/entity/TicketQueryDslRepository.java @@ -20,7 +20,14 @@ public class TicketQueryDslRepository { private final JPAQueryFactory queryFactory; @Lock(value = LockModeType.PESSIMISTIC_WRITE) - public Optional findByIdWithLock(Long id) { + public Optional findByIdWithPessimisticLock(Long id) { + return Optional.ofNullable(queryFactory.selectFrom(ticket) + .where(ticket.id.eq(id)) + .fetchOne()); + } + + @Lock(value = LockModeType.OPTIMISTIC) + public Optional findByIdWithOptimisticLock(Long id) { return Optional.ofNullable(queryFactory.selectFrom(ticket) .where(ticket.id.eq(id)) .fetchOne()); diff --git a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java index b9fb7d9..4deff27 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java +++ b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java @@ -27,8 +27,14 @@ public Ticket getTicket(Long id) { } @Transactional(readOnly = true) - public Ticket getTicketWithLock(Long id) { - return ticketQueryDslRepository.findByIdWithLock(id) + public Ticket getTicketWithPessimisticLock(Long id) { + return ticketQueryDslRepository.findByIdWithPessimisticLock(id) + .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + } + + @Transactional(readOnly = true) + public Ticket getTicketWithOptimisticLock(Long id) { + return ticketQueryDslRepository.findByIdWithOptimisticLock(id) .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); } } diff --git a/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java new file mode 100644 index 0000000..464e669 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java @@ -0,0 +1,73 @@ +package com.week.zumgnmarket.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.order.controller.OrderFacade; +import com.week.zumgnmarket.order.dto.OrderRequest; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@SpringBootTest +public class OptimisticLockOrderServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private OrderFacade orderFacade; + + private User 회원; + private Ticket 티켓; + private final int thread = 1000; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @AfterEach + void after() { + userRepository.deleteAll(); + ticketRepository.deleteAll(); + } + + @Test + void 동시에_티켓_주문() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(123); + CountDownLatch countDownLatch = new CountDownLatch(thread); + + //when + for (int i = 0; i < thread; i++) { + executorService.execute(() -> { + //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 + try { + orderFacade.orderWithOLock(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + //then + assertEquals(0, 티켓.getQuantity()); + } +} diff --git a/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java index 79d3797..43e33e7 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java @@ -58,7 +58,7 @@ void after() { for (int i = 0; i < thread; i++) { executorService.execute(() -> { //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 - orderService.orderTicket(회원, ticketService.getTicketWithLock(티켓.getId()), 1); + orderService.orderTicket(회원, ticketService.getTicketWithPessimisticLock(티켓.getId()), 1); countDownLatch.countDown(); }); } From 2b88904759aa862c7b8e27c1c8f22caadc2a5159 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Thu, 23 Mar 2023 00:44:05 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20RedissonLock=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(error)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../order/controller/OrderFacade.java | 27 +++++++- .../zumgnmarket/order/dto/OrderResponse.java | 2 + .../OptimisticLockOrderServiceTest.java | 2 +- .../service/RedissonLockOrderServiceTest.java | 69 +++++++++++++++++++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java diff --git a/build.gradle b/build.gradle index 2eb8618..e9acc9c 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,9 @@ dependencies { // 쿼리 파라미터 로그 implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8' + //Redisson + implementation 'org.redisson:redisson-spring-boot-starter:3.17.4' + compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java index 3096233..37bc0b4 100644 --- a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java @@ -1,5 +1,9 @@ package com.week.zumgnmarket.order.controller; +import java.util.concurrent.TimeUnit; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import com.week.zumgnmarket.order.dto.OrderRequest; @@ -20,6 +24,7 @@ public class OrderFacade { private final UserService userService; private final TicketService ticketService; private final OrderService orderService; + private final RedissonClient redissonClient; public OrderResponse order(OrderRequest request) { User user = userService.getUser(request.getUserId()); @@ -29,7 +34,7 @@ public OrderResponse order(OrderRequest request) { return OrderResponse.of(order, user, ticket); } - public OrderResponse orderWithPLock(OrderRequest request) { + public OrderResponse orderWithPessimisticLock(OrderRequest request) { User user = userService.getUser(request.getUserId()); Ticket ticket = ticketService.getTicketWithPessimisticLock(request.getTicketId()); Order order = orderService.orderTicket(user, ticket, request.getQuantity()); @@ -37,7 +42,7 @@ public OrderResponse orderWithPLock(OrderRequest request) { return OrderResponse.of(order, user, ticket); } - public OrderResponse orderWithOLock(OrderRequest request) throws InterruptedException { + public OrderResponse orderWithOptimisticLock(OrderRequest request) throws InterruptedException { User user = userService.getUser(request.getUserId()); //OptimisticLock 같은 경우 실패했을 때 재시도를 위해 while 을 사용하였습니다. @@ -51,4 +56,22 @@ public OrderResponse orderWithOLock(OrderRequest request) throws InterruptedExce } } } + + public OrderResponse orderWithRedisson(OrderRequest request) { + User user = userService.getUser(request.getUserId()); + RLock lock = redissonClient.getLock(request.getTicketId().toString()); + + try { + if (!lock.tryLock(2, 1, TimeUnit.SECONDS)) { + return new OrderResponse(); + } + Ticket ticket = ticketService.getTicket(request.getTicketId()); + Order order = orderService.orderTicket(user, ticket, request.getQuantity()); + return OrderResponse.of(order, user, ticket); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + } } diff --git a/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java b/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java index 003a599..4b1e9e6 100644 --- a/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java +++ b/src/main/java/com/week/zumgnmarket/order/dto/OrderResponse.java @@ -6,8 +6,10 @@ import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor public class OrderResponse { public Long orderId; public Long userId; diff --git a/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java index 464e669..ed15184 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java @@ -58,7 +58,7 @@ void after() { executorService.execute(() -> { //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 try { - orderFacade.orderWithOLock(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + orderFacade.orderWithOptimisticLock(new OrderRequest(회원.getId(), 티켓.getId(), 1)); } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java new file mode 100644 index 0000000..99109e0 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java @@ -0,0 +1,69 @@ +package com.week.zumgnmarket.order.service; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.order.controller.OrderFacade; +import com.week.zumgnmarket.order.dto.OrderRequest; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.entity.UserRepository; + +@SpringBootTest +public class RedissonLockOrderServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private OrderFacade orderFacade; + + private User 회원; + private Ticket 티켓; + private final int thread = 1000; + + @BeforeEach + void setup() { + 회원 = new User("kim", "email@email.com", "pwd"); + 티켓 = new Ticket("CAT'S", 10000, 1000); + userRepository.save(회원); + ticketRepository.save(티켓); + } + + @AfterEach + void after() { + userRepository.deleteAll(); + ticketRepository.deleteAll(); + } + + @Test + void 동시에_티켓_주문() throws InterruptedException { + //given + ExecutorService executorService = Executors.newFixedThreadPool(123); + CountDownLatch countDownLatch = new CountDownLatch(thread); + + //when + for (int i = 0; i < thread; i++) { + executorService.execute(() -> { + //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 + orderFacade.orderWithRedisson(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + //then + assertEquals(0, 티켓.getQuantity()); + } +} From 703a852015d9062f6e3c2d997f522c844bed312e Mon Sep 17 00:00:00 2001 From: yyy96 Date: Thu, 23 Mar 2023 00:59:41 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20error=20?= =?UTF-8?q?message=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../week/zumgnmarket/ticket/exception/ErrorMessage.java | 5 +++++ .../com/week/zumgnmarket/ticket/service/TicketService.java | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/ticket/exception/ErrorMessage.java diff --git a/src/main/java/com/week/zumgnmarket/ticket/exception/ErrorMessage.java b/src/main/java/com/week/zumgnmarket/ticket/exception/ErrorMessage.java new file mode 100644 index 0000000..48ed6eb --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/ticket/exception/ErrorMessage.java @@ -0,0 +1,5 @@ +package com.week.zumgnmarket.ticket.exception; + +public class ErrorMessage { + public static final String TICKET_NOT_FOUND_BY_ID = "Ticket not found with id "; +} diff --git a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java index 4deff27..17a8d60 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java +++ b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java @@ -8,6 +8,7 @@ import com.week.zumgnmarket.ticket.entity.Ticket; import com.week.zumgnmarket.ticket.entity.TicketQueryDslRepository; import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.ticket.exception.ErrorMessage; import lombok.RequiredArgsConstructor; @@ -23,18 +24,18 @@ public class TicketService { @Transactional(readOnly = true) public Ticket getTicket(Long id) { return ticketRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + .orElseThrow(() -> new EntityNotFoundException(ErrorMessage.TICKET_NOT_FOUND_BY_ID + id)); } @Transactional(readOnly = true) public Ticket getTicketWithPessimisticLock(Long id) { return ticketQueryDslRepository.findByIdWithPessimisticLock(id) - .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + .orElseThrow(() -> new EntityNotFoundException(ErrorMessage.TICKET_NOT_FOUND_BY_ID + id)); } @Transactional(readOnly = true) public Ticket getTicketWithOptimisticLock(Long id) { return ticketQueryDslRepository.findByIdWithOptimisticLock(id) - .orElseThrow(() -> new EntityNotFoundException("Ticket not found with id: " + id)); + .orElseThrow(() -> new EntityNotFoundException(ErrorMessage.TICKET_NOT_FOUND_BY_ID + id)); } } From ab5189d6fcabf2920e824ecb6cb9bbf43177b470 Mon Sep 17 00:00:00 2001 From: yyy96 Date: Sat, 25 Mar 2023 00:08:45 +0900 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EA=B0=90=EC=A7=80=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(=EB=8D=B0=EB=93=9C=EB=9D=BD=20=EB=AF=B8?= =?UTF-8?q?=ED=95=B4=EA=B2=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++ .../order/controller/OrderFacade.java | 2 + .../ticket/service/TicketService.java | 4 ++ .../zumgnmarket/user/service/UserService.java | 7 +++ .../order/controller/OrderFacadeTest.java | 46 +++++++++++++++++++ .../OptimisticLockOrderServiceTest.java | 11 ++++- .../PessimisticLockOrderServiceTest.java | 14 ++++-- .../service/RedissonLockOrderServiceTest.java | 11 ++++- 8 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/week/zumgnmarket/order/controller/OrderFacadeTest.java diff --git a/build.gradle b/build.gradle index e9acc9c..7c43456 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,11 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + + //test testImplementation 'org.springframework.boot:spring-boot-starter-test' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java index 37bc0b4..3522460 100644 --- a/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java +++ b/src/main/java/com/week/zumgnmarket/order/controller/OrderFacade.java @@ -5,6 +5,7 @@ import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import com.week.zumgnmarket.order.dto.OrderRequest; import com.week.zumgnmarket.order.dto.OrderResponse; @@ -18,6 +19,7 @@ import lombok.RequiredArgsConstructor; @Component +@Transactional @RequiredArgsConstructor public class OrderFacade { diff --git a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java index 17a8d60..e6e7bc4 100644 --- a/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java +++ b/src/main/java/com/week/zumgnmarket/ticket/service/TicketService.java @@ -21,6 +21,10 @@ public class TicketService { private final TicketQueryDslRepository ticketQueryDslRepository; + public Ticket save(Ticket ticket){ + return ticketRepository.save(ticket); + } + @Transactional(readOnly = true) public Ticket getTicket(Long id) { return ticketRepository.findById(id) diff --git a/src/main/java/com/week/zumgnmarket/user/service/UserService.java b/src/main/java/com/week/zumgnmarket/user/service/UserService.java index 431a4ed..97e8643 100644 --- a/src/main/java/com/week/zumgnmarket/user/service/UserService.java +++ b/src/main/java/com/week/zumgnmarket/user/service/UserService.java @@ -3,6 +3,7 @@ import javax.persistence.EntityNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.week.zumgnmarket.user.entity.User; import com.week.zumgnmarket.user.entity.UserRepository; @@ -10,11 +11,17 @@ import lombok.RequiredArgsConstructor; @Service +@Transactional @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + public User save(User user){ + return userRepository.save(user); + } + + @Transactional(readOnly = true) public User getUser(Long id) { return userRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + id)); diff --git a/src/test/java/com/week/zumgnmarket/order/controller/OrderFacadeTest.java b/src/test/java/com/week/zumgnmarket/order/controller/OrderFacadeTest.java new file mode 100644 index 0000000..afdcac8 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/order/controller/OrderFacadeTest.java @@ -0,0 +1,46 @@ +package com.week.zumgnmarket.order.controller; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.week.zumgnmarket.order.dto.OrderRequest; +import com.week.zumgnmarket.ticket.entity.Ticket; +import com.week.zumgnmarket.ticket.service.TicketService; +import com.week.zumgnmarket.user.entity.User; +import com.week.zumgnmarket.user.service.UserService; + +@SpringBootTest +public class OrderFacadeTest { + @Autowired + private UserService userService; + @Autowired + private TicketService ticketService; + @Autowired + private OrderFacade orderFacade; + + /** + * 학습 테스트) + * 해당 엔티티가 트랜잭션에 따라 변경 감지가 일어나는 조건을 테스트. + * 만약 orderFacade 에 @Transactional 이 없다면 해당 테스트는 실패합니다. + * 또한 static '티켓'은 order 의 Ticket 과 다른 트랜잭션, 영속성 컨텍스트에 존재하기 때문에 변경감지가 발생하지 않습니다. + * */ + @Test + void 영속성_컨텍스트_변경감지_테스트() { + //given + User 회원 = new User("kim", "email@email.com", "pwd"); + Ticket 티켓 = new Ticket("CAT'S", 10000, 1000); + userService.save(회원); + ticketService.save(티켓); + + //when + orderFacade.order(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + + //then + Ticket result = ticketService.getTicket(티켓.getId()); + assertEquals(result.getQuantity(), 999); + assertNotEquals(티켓.getQuantity(), 999); + } +} diff --git a/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java index ed15184..6d4d319 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/OptimisticLockOrderServiceTest.java @@ -16,9 +16,13 @@ import com.week.zumgnmarket.order.dto.OrderRequest; import com.week.zumgnmarket.ticket.entity.Ticket; import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.ticket.service.TicketService; import com.week.zumgnmarket.user.entity.User; import com.week.zumgnmarket.user.entity.UserRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @SpringBootTest public class OptimisticLockOrderServiceTest { @@ -27,6 +31,8 @@ public class OptimisticLockOrderServiceTest { @Autowired private TicketRepository ticketRepository; @Autowired + private TicketService ticketService; + @Autowired private OrderFacade orderFacade; private User 회원; @@ -56,9 +62,9 @@ void after() { //when for (int i = 0; i < thread; i++) { executorService.execute(() -> { - //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 try { orderFacade.orderWithOptimisticLock(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + log.info("남은 티켓 수량 : " + ticketService.getTicket(티켓.getId()).getQuantity()); } catch (InterruptedException e) { e.printStackTrace(); } @@ -68,6 +74,7 @@ void after() { countDownLatch.await(); //then - assertEquals(0, 티켓.getQuantity()); + Ticket result = ticketService.getTicket(티켓.getId()); + assertEquals(result.getQuantity(), 0); } } diff --git a/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java index 43e33e7..7e0f60e 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/PessimisticLockOrderServiceTest.java @@ -12,12 +12,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import com.week.zumgnmarket.order.controller.OrderFacade; +import com.week.zumgnmarket.order.dto.OrderRequest; import com.week.zumgnmarket.ticket.entity.Ticket; import com.week.zumgnmarket.ticket.entity.TicketRepository; import com.week.zumgnmarket.ticket.service.TicketService; import com.week.zumgnmarket.user.entity.User; import com.week.zumgnmarket.user.entity.UserRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @SpringBootTest public class PessimisticLockOrderServiceTest { @@ -28,7 +33,7 @@ public class PessimisticLockOrderServiceTest { @Autowired private TicketService ticketService; @Autowired - private PessimisticLockOrderService orderService; + private OrderFacade orderFacade; private User 회원; private Ticket 티켓; @@ -57,14 +62,15 @@ void after() { //when for (int i = 0; i < thread; i++) { executorService.execute(() -> { - //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 - orderService.orderTicket(회원, ticketService.getTicketWithPessimisticLock(티켓.getId()), 1); + orderFacade.orderWithPessimisticLock(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + log.info("남은 티켓 수량 : " + ticketService.getTicket(티켓.getId()).getQuantity()); countDownLatch.countDown(); }); } countDownLatch.await(); //then - assertEquals(0, 티켓.getQuantity()); + Ticket result = ticketService.getTicket(티켓.getId()); + assertEquals(result.getQuantity(), 0); } } diff --git a/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java b/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java index 99109e0..8ed3f52 100644 --- a/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java +++ b/src/test/java/com/week/zumgnmarket/order/service/RedissonLockOrderServiceTest.java @@ -16,9 +16,13 @@ import com.week.zumgnmarket.order.dto.OrderRequest; import com.week.zumgnmarket.ticket.entity.Ticket; import com.week.zumgnmarket.ticket.entity.TicketRepository; +import com.week.zumgnmarket.ticket.service.TicketService; import com.week.zumgnmarket.user.entity.User; import com.week.zumgnmarket.user.entity.UserRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j @SpringBootTest public class RedissonLockOrderServiceTest { @@ -27,6 +31,8 @@ public class RedissonLockOrderServiceTest { @Autowired private TicketRepository ticketRepository; @Autowired + private TicketService ticketService; + @Autowired private OrderFacade orderFacade; private User 회원; @@ -56,14 +62,15 @@ void after() { //when for (int i = 0; i < thread; i++) { executorService.execute(() -> { - //TODO: 미해결 - 변경감지가 일어나지 않는 이슈 orderFacade.orderWithRedisson(new OrderRequest(회원.getId(), 티켓.getId(), 1)); + log.info("남은 티켓 수량 : " + ticketService.getTicket(티켓.getId()).getQuantity()); countDownLatch.countDown(); }); } countDownLatch.await(); //then - assertEquals(0, 티켓.getQuantity()); + Ticket result = ticketService.getTicket(티켓.getId()); + assertEquals(result.getQuantity(), 0); } }