From c860195a1f492fa749fad58a6c4ff0d32ba53383 Mon Sep 17 00:00:00 2001 From: youngji503 Date: Sun, 19 Mar 2023 10:41:59 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=85=8B=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e69de29..67b4ffc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/zumterpark?useSSL=false&serverTimezone=Asia/Seoul + username: root + password: + jpa: + database: mysql + show-sql: true + hibernate: + ddl-auto: update +logging: + level: + org.hibernate.sql: debug + org.hibernate.type: trace \ No newline at end of file From ec8ef02d95a10e1a973617e04e7513670c906e17 Mon Sep 17 00:00:00 2001 From: youngji503 Date: Sun, 19 Mar 2023 11:22:46 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Redisson=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98=20=EB=B0=8F=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ docker-compose.yml | 16 +++++++++++ .../zumgnmarket/config/RedissonConfig.java | 28 +++++++++++++++++++ src/main/resources/application.yml | 3 ++ 4 files changed, 50 insertions(+) create mode 100644 docker-compose.yml create mode 100644 src/main/java/com/week/zumgnmarket/config/RedissonConfig.java diff --git a/build.gradle b/build.gradle index 2eb8618..5a2b58e 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2f28383 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.9' +services: + redis: + image: 'redis:alpine' + hostname: redis + container_name: redis-dailyProject + ports: + - '6379:6379' + +volumes: + redis: + driver: local + +networks: + mysqlnetwork: + driver: bridge diff --git a/src/main/java/com/week/zumgnmarket/config/RedissonConfig.java b/src/main/java/com/week/zumgnmarket/config/RedissonConfig.java new file mode 100644 index 0000000..b05c11f --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/config/RedissonConfig.java @@ -0,0 +1,28 @@ +package com.week.zumgnmarket.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + @Value("${spring.redis.host}") + private String redisHost; + + @Value("${spring.redis.port}") + private int redisPort; + + private static final String REDISSON_HOST_PREFIX = "redis://"; + + @Bean + public RedissonClient redissonClient() { + RedissonClient redisson = null; + Config config = new Config(); + config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort); + redisson = Redisson.create(config); + return redisson; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 67b4ffc..740c9b9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,7 @@ spring: + redis: + host: localhost + port: 6379 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/zumterpark?useSSL=false&serverTimezone=Asia/Seoul From fe40dcaa4977c53a79d2b223a80b1db083bd1124 Mon Sep 17 00:00:00 2001 From: youngji503 Date: Sun, 19 Mar 2023 14:25:58 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=ED=8B=B0=EC=BC=93=ED=8C=85=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 4 -- .../zumgnmarket/config/QueryDslConfig.java | 21 +++++++++ .../controller/TicketController.java | 22 ++++++++++ .../com/week/zumgnmarket/entity/Buyer.java | 39 +++++++++++++++++ .../com/week/zumgnmarket/entity/Musical.java | 25 +++++++++++ .../zumgnmarket/entity/MusicalTicket.java | 43 +++++++++++++++++++ .../week/zumgnmarket/fecade/TicketFacade.java | 27 ++++++++++++ .../zumgnmarket/fecade/dto/TicketRequest.java | 16 +++++++ .../repository/BuyerJpaRepository.java | 8 ++++ .../repository/MusicalJpaRepository.java | 7 +++ .../repository/TicketJpaRepository.java | 10 +++++ .../zumgnmarket/service/BuyerService.java | 21 +++++++++ .../service/MusicalTicketService.java | 41 ++++++++++++++++++ 13 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/config/QueryDslConfig.java create mode 100644 src/main/java/com/week/zumgnmarket/controller/TicketController.java create mode 100644 src/main/java/com/week/zumgnmarket/entity/Buyer.java create mode 100644 src/main/java/com/week/zumgnmarket/entity/Musical.java create mode 100644 src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java create mode 100644 src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java create mode 100644 src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java create mode 100644 src/main/java/com/week/zumgnmarket/repository/BuyerJpaRepository.java create mode 100644 src/main/java/com/week/zumgnmarket/repository/MusicalJpaRepository.java create mode 100644 src/main/java/com/week/zumgnmarket/repository/TicketJpaRepository.java create mode 100644 src/main/java/com/week/zumgnmarket/service/BuyerService.java create mode 100644 src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java diff --git a/docker-compose.yml b/docker-compose.yml index 2f28383..3b36782 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,3 @@ services: volumes: redis: driver: local - -networks: - mysqlnetwork: - driver: bridge diff --git a/src/main/java/com/week/zumgnmarket/config/QueryDslConfig.java b/src/main/java/com/week/zumgnmarket/config/QueryDslConfig.java new file mode 100644 index 0000000..0665aad --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/config/QueryDslConfig.java @@ -0,0 +1,21 @@ +package com.week.zumgnmarket.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} + diff --git a/src/main/java/com/week/zumgnmarket/controller/TicketController.java b/src/main/java/com/week/zumgnmarket/controller/TicketController.java new file mode 100644 index 0000000..084a953 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/controller/TicketController.java @@ -0,0 +1,22 @@ +package com.week.zumgnmarket.controller; + +import com.week.zumgnmarket.fecade.TicketFacade; +import com.week.zumgnmarket.fecade.dto.TicketRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/tickets") +@RequiredArgsConstructor +public class TicketController { + + private final TicketFacade ticketFacade; + + @GetMapping("/buy") + public void buy(@RequestBody TicketRequest ticketRequest) { + ticketFacade.buyTicket(ticketRequest); + } +} diff --git a/src/main/java/com/week/zumgnmarket/entity/Buyer.java b/src/main/java/com/week/zumgnmarket/entity/Buyer.java new file mode 100644 index 0000000..269b35d --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/entity/Buyer.java @@ -0,0 +1,39 @@ +package com.week.zumgnmarket.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Table(name = "buyer") +public class Buyer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "buyer_idx") + private Integer id; + + @Column(name = "nick_name") + private String nickName; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "musical_idx") + private List musicals = new ArrayList(); + + public void updateMusicals(Musical musical) { + this.musicals.add(musical); + } + + public boolean isExistMusicals(Musical musical) { + return this.musicals.contains(musical); + } + +} diff --git a/src/main/java/com/week/zumgnmarket/entity/Musical.java b/src/main/java/com/week/zumgnmarket/entity/Musical.java new file mode 100644 index 0000000..bd8bc69 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/entity/Musical.java @@ -0,0 +1,25 @@ +package com.week.zumgnmarket.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Table(name = "musical") +public class Musical { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "musical_idx") + private Integer id; + + @Column(name = "title") + private String title; + + @Column(name = "description") + private String description; +} diff --git a/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java b/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java new file mode 100644 index 0000000..75ac015 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java @@ -0,0 +1,43 @@ +package com.week.zumgnmarket.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDate; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Table(name = "musical_ticket") +public class MusicalTicket { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ticket_idx") + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "musicla_idx") + private Musical musical; + + @Column(name = "ticket_count") + private Long ticketCount; + + @Column(name = "ticketing_date") + private LocalDate ticketingDate; + + public void decrease() { + if((this.ticketCount - 1) < 0 ){ + throw new RuntimeException("오늘자 티켓팅이 마감되었습니다 (재고 부족)"); + } + this.ticketCount -= ticketCount; + } + + public boolean checkTicketingDate(LocalDate localDate) { + return this.ticketingDate.equals(localDate); + } +} diff --git a/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java b/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java new file mode 100644 index 0000000..72d2d0c --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java @@ -0,0 +1,27 @@ +package com.week.zumgnmarket.fecade; + +import com.week.zumgnmarket.entity.Buyer; +import com.week.zumgnmarket.entity.Musical; +import com.week.zumgnmarket.entity.MusicalTicket; +import com.week.zumgnmarket.fecade.dto.TicketRequest; +import com.week.zumgnmarket.service.BuyerService; +import com.week.zumgnmarket.service.MusicalTicketService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TicketFacade { + private final MusicalTicketService musicalTicketService; + private final BuyerService buyerService; + + public void buyTicket(TicketRequest ticketRequest) { + Buyer buyer = buyerService.findBuyerById(ticketRequest.getBuyerId()); + Musical musical = musicalTicketService.findMusicalById(ticketRequest.getMusicalId()); + MusicalTicket musicalTicket = musicalTicketService.findTicketByMusical(musical); + if (!musicalTicket.checkTicketingDate(ticketRequest.getTicketingDate())) { + throw new RuntimeException("지금은 티켓팅 기간이 아닙니다."); + } + musicalTicketService.buyTicket(buyer, musicalTicket); + } +} diff --git a/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java new file mode 100644 index 0000000..79f4a71 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java @@ -0,0 +1,16 @@ +package com.week.zumgnmarket.fecade.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; + +@Getter +@Setter +@Builder +public class TicketRequest { + private Integer buyerId; + private Integer musicalId; + private LocalDate TicketingDate; +} diff --git a/src/main/java/com/week/zumgnmarket/repository/BuyerJpaRepository.java b/src/main/java/com/week/zumgnmarket/repository/BuyerJpaRepository.java new file mode 100644 index 0000000..efea925 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/repository/BuyerJpaRepository.java @@ -0,0 +1,8 @@ +package com.week.zumgnmarket.repository; + +import com.week.zumgnmarket.entity.Buyer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BuyerJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/week/zumgnmarket/repository/MusicalJpaRepository.java b/src/main/java/com/week/zumgnmarket/repository/MusicalJpaRepository.java new file mode 100644 index 0000000..49fc3c9 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/repository/MusicalJpaRepository.java @@ -0,0 +1,7 @@ +package com.week.zumgnmarket.repository; + +import com.week.zumgnmarket.entity.Musical; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MusicalJpaRepository extends JpaRepository { +} diff --git a/src/main/java/com/week/zumgnmarket/repository/TicketJpaRepository.java b/src/main/java/com/week/zumgnmarket/repository/TicketJpaRepository.java new file mode 100644 index 0000000..449ff7c --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/repository/TicketJpaRepository.java @@ -0,0 +1,10 @@ +package com.week.zumgnmarket.repository; + +import com.week.zumgnmarket.entity.Musical; +import com.week.zumgnmarket.entity.MusicalTicket; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TicketJpaRepository extends JpaRepository { + MusicalTicket findByMusical(Musical musical); +} diff --git a/src/main/java/com/week/zumgnmarket/service/BuyerService.java b/src/main/java/com/week/zumgnmarket/service/BuyerService.java new file mode 100644 index 0000000..621acb6 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/service/BuyerService.java @@ -0,0 +1,21 @@ +package com.week.zumgnmarket.service; + +import com.week.zumgnmarket.entity.Buyer; +import com.week.zumgnmarket.repository.BuyerJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class BuyerService { + + private final BuyerJpaRepository buyerJpaRepository; + + @Transactional(readOnly = true) + public Buyer findBuyerById(Integer id) { + return buyerJpaRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다. idx: " + id)); + } +} diff --git a/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java b/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java new file mode 100644 index 0000000..43b6c44 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java @@ -0,0 +1,41 @@ +package com.week.zumgnmarket.service; + +import com.week.zumgnmarket.entity.Buyer; +import com.week.zumgnmarket.entity.Musical; +import com.week.zumgnmarket.entity.MusicalTicket; +import com.week.zumgnmarket.repository.MusicalJpaRepository; +import com.week.zumgnmarket.repository.TicketJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MusicalTicketService { + + private final TicketJpaRepository ticketJpaRepository; + + private final MusicalJpaRepository musicalJpaRepository; + + public MusicalTicket createTicket(MusicalTicket ticket) { + return ticketJpaRepository.save(ticket); + } + @Transactional(readOnly = true) + public MusicalTicket findTicketByMusical(Musical musical) { + return ticketJpaRepository.findByMusical(musical); + } + + public void buyTicket(Buyer buyer, MusicalTicket musicalTicket) { + Musical musical = musicalTicket.getMusical(); + if (!buyer.isExistMusicals(musical)) { + throw new RuntimeException("이미 해당 뮤지컬 티켓을 구매하셨습니다. "); + } + musicalTicket.decrease(); + buyer.updateMusicals(musicalTicket.getMusical()); + } + @Transactional(readOnly = true) + public Musical findMusicalById(Integer id) { + return musicalJpaRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 뮤지컬 입니다. ")); + } +} From 11a739392f5a2d46f1a9661dc107efcb2033d1d7 Mon Sep 17 00:00:00 2001 From: youngji503 Date: Sun, 19 Mar 2023 15:58:43 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EA=B5=AC=EB=A7=A4=20entity=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/week/zumgnmarket/entity/Purchase.java | 41 +++++++++++++++++++ .../repository/PurchaseJpaRepository.java | 10 +++++ 2 files changed, 51 insertions(+) create mode 100644 src/main/java/com/week/zumgnmarket/entity/Purchase.java create mode 100644 src/main/java/com/week/zumgnmarket/repository/PurchaseJpaRepository.java diff --git a/src/main/java/com/week/zumgnmarket/entity/Purchase.java b/src/main/java/com/week/zumgnmarket/entity/Purchase.java new file mode 100644 index 0000000..9abaf33 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/entity/Purchase.java @@ -0,0 +1,41 @@ +package com.week.zumgnmarket.entity; + +import lombok.*; + +import javax.persistence.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +@Builder +@Table(name = "purchase") +public class Purchase { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "purchase") + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "buyer_idx") + private Buyer buyer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "musical_idx") + private Musical musical; + + public static Purchase of(Buyer buyer, Musical musical) { + return Purchase.builder() + .buyer(buyer) + .musical(musical) + .build(); + } + + public Integer getBuyerId() { + return this.buyer.getId(); + } + + public String getTitle() { + return this.musical.getTitle(); + } +} diff --git a/src/main/java/com/week/zumgnmarket/repository/PurchaseJpaRepository.java b/src/main/java/com/week/zumgnmarket/repository/PurchaseJpaRepository.java new file mode 100644 index 0000000..7d20539 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/repository/PurchaseJpaRepository.java @@ -0,0 +1,10 @@ +package com.week.zumgnmarket.repository; + +import com.week.zumgnmarket.entity.Buyer; +import com.week.zumgnmarket.entity.Musical; +import com.week.zumgnmarket.entity.Purchase; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PurchaseJpaRepository extends JpaRepository { + boolean existsByBuyerAndMusical(Buyer buyer, Musical musical); +} From 3b071132cb997dfcedf59fca543e99e5c04e435e Mon Sep 17 00:00:00 2001 From: youngji503 Date: Sun, 19 Mar 2023 19:27:11 +0900 Subject: [PATCH 5/5] =?UTF-8?q?RLock=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20&=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1=20(=EC=8B=A4=ED=8C=A8=20,=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=BC=80=EC=9D=B4=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TicketController.java | 6 +- .../com/week/zumgnmarket/entity/Buyer.java | 21 +-- .../com/week/zumgnmarket/entity/Musical.java | 13 +- .../zumgnmarket/entity/MusicalTicket.java | 28 +++- .../com/week/zumgnmarket/entity/Purchase.java | 2 +- .../week/zumgnmarket/fecade/TicketFacade.java | 37 ++++- .../zumgnmarket/fecade/dto/TicketRequest.java | 7 + .../fecade/dto/TicketResponse.java | 22 +++ .../service/MusicalTicketService.java | 32 ++-- src/main/resources/application.yml | 7 +- .../zumgnmarket/fecade/TicketFacadeTest.java | 146 ++++++++++++++++++ 11 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/week/zumgnmarket/fecade/dto/TicketResponse.java create mode 100644 src/test/java/com/week/zumgnmarket/fecade/TicketFacadeTest.java diff --git a/src/main/java/com/week/zumgnmarket/controller/TicketController.java b/src/main/java/com/week/zumgnmarket/controller/TicketController.java index 084a953..b84f2b1 100644 --- a/src/main/java/com/week/zumgnmarket/controller/TicketController.java +++ b/src/main/java/com/week/zumgnmarket/controller/TicketController.java @@ -2,7 +2,9 @@ import com.week.zumgnmarket.fecade.TicketFacade; import com.week.zumgnmarket.fecade.dto.TicketRequest; +import com.week.zumgnmarket.fecade.dto.TicketResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,7 +18,7 @@ public class TicketController { private final TicketFacade ticketFacade; @GetMapping("/buy") - public void buy(@RequestBody TicketRequest ticketRequest) { - ticketFacade.buyTicket(ticketRequest); + public ResponseEntity buy(@RequestBody TicketRequest ticketRequest) { + return ResponseEntity.ok(ticketFacade.buy(ticketRequest)); } } diff --git a/src/main/java/com/week/zumgnmarket/entity/Buyer.java b/src/main/java/com/week/zumgnmarket/entity/Buyer.java index 269b35d..33a9ecf 100644 --- a/src/main/java/com/week/zumgnmarket/entity/Buyer.java +++ b/src/main/java/com/week/zumgnmarket/entity/Buyer.java @@ -1,18 +1,14 @@ package com.week.zumgnmarket.entity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter +@Builder @Table(name = "buyer") public class Buyer { @@ -24,16 +20,9 @@ public class Buyer { @Column(name = "nick_name") private String nickName; - @OneToMany(fetch = FetchType.LAZY) - @JoinColumn(name = "musical_idx") - private List musicals = new ArrayList(); - - public void updateMusicals(Musical musical) { - this.musicals.add(musical); - } - - public boolean isExistMusicals(Musical musical) { - return this.musicals.contains(musical); + public static Buyer of(String nickName) { + return Buyer.builder() + .nickName(nickName).build(); } } diff --git a/src/main/java/com/week/zumgnmarket/entity/Musical.java b/src/main/java/com/week/zumgnmarket/entity/Musical.java index bd8bc69..afebf5e 100644 --- a/src/main/java/com/week/zumgnmarket/entity/Musical.java +++ b/src/main/java/com/week/zumgnmarket/entity/Musical.java @@ -1,14 +1,14 @@ package com.week.zumgnmarket.entity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; +import lombok.*; import javax.persistence.*; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor +@Getter +@Builder @Table(name = "musical") public class Musical { @@ -22,4 +22,11 @@ public class Musical { @Column(name = "description") private String description; + + public static Musical of(String title, String description) { + return Musical.builder() + .title(title) + .description(description) + .build(); + } } diff --git a/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java b/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java index 75ac015..1596bb3 100644 --- a/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java +++ b/src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java @@ -1,9 +1,7 @@ package com.week.zumgnmarket.entity; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.extern.slf4j.Slf4j; import javax.persistence.*; import java.time.LocalDate; @@ -12,6 +10,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter +@Slf4j +@Builder @Table(name = "musical_ticket") public class MusicalTicket { @@ -30,14 +30,30 @@ public class MusicalTicket { @Column(name = "ticketing_date") private LocalDate ticketingDate; + public static MusicalTicket of(Musical musical, Long ticketCount, LocalDate ticketingDate) { + return MusicalTicket.builder() + .musical(musical) + .ticketCount(ticketCount) + .ticketingDate(ticketingDate) + .build(); + } + public void decrease() { + final String thread = Thread.currentThread().getName(); if((this.ticketCount - 1) < 0 ){ - throw new RuntimeException("오늘자 티켓팅이 마감되었습니다 (재고 부족)"); + //throw new RuntimeException("오늘자 티켓팅이 마감되었습니다 (재고 부족)"); - 테스트 위해 주석 + log.error("진행중인 사람 : {}, 메세지 : 티켓팅이 마감되었습니다(재고 부족 - {} 개) ", thread, this.ticketCount); + return; } - this.ticketCount -= ticketCount; + this.ticketCount = this.ticketCount - 1; + log.error("진행중인 사람 : {}, 메세지 : 티켓팅을 성공하셨습니다(남은 티켓 갯수 - {} 개) ", thread, this.ticketCount); } public boolean checkTicketingDate(LocalDate localDate) { return this.ticketingDate.equals(localDate); } + + public Integer getMusicalId() { + return this.musical.getId(); + } } diff --git a/src/main/java/com/week/zumgnmarket/entity/Purchase.java b/src/main/java/com/week/zumgnmarket/entity/Purchase.java index 9abaf33..56c8bf1 100644 --- a/src/main/java/com/week/zumgnmarket/entity/Purchase.java +++ b/src/main/java/com/week/zumgnmarket/entity/Purchase.java @@ -13,7 +13,7 @@ public class Purchase { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "purchase") + @Column(name = "purchase_idx") private Integer id; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java b/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java index 72d2d0c..15cfb3c 100644 --- a/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java +++ b/src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java @@ -3,25 +3,58 @@ import com.week.zumgnmarket.entity.Buyer; import com.week.zumgnmarket.entity.Musical; import com.week.zumgnmarket.entity.MusicalTicket; +import com.week.zumgnmarket.entity.Purchase; import com.week.zumgnmarket.fecade.dto.TicketRequest; +import com.week.zumgnmarket.fecade.dto.TicketResponse; import com.week.zumgnmarket.service.BuyerService; import com.week.zumgnmarket.service.MusicalTicketService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; +import java.util.concurrent.TimeUnit; + +@Slf4j @Component @RequiredArgsConstructor public class TicketFacade { private final MusicalTicketService musicalTicketService; private final BuyerService buyerService; + private final RedissonClient redissonClient; + + public TicketResponse buy(TicketRequest ticketRequest) { + Buyer buyer = buyerService.findBuyerById(ticketRequest.getBuyerId()); + Musical musical = musicalTicketService.findMusicalById(ticketRequest.getMusicalId()); + MusicalTicket musicalTicket = musicalTicketService.findTicketByMusical(musical); + if (!musicalTicket.checkTicketingDate(ticketRequest.getTicketingDate())) { + throw new RuntimeException("지금은 티켓팅 기간이 아닙니다."); + } + RLock lock = redissonClient.getLock(musical.getTitle() + ":" + ticketRequest.getTicketingDate()); + try { + if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) { + return new TicketResponse(); + } + Purchase purchase = musicalTicketService.buyTicket(buyer, musicalTicket.getId()); + return TicketResponse.of(purchase); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + //lock.isLocked() && lock.isHeldByCurrentThread() + if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } - public void buyTicket(TicketRequest ticketRequest) { + public void buyNoLock(TicketRequest ticketRequest) { Buyer buyer = buyerService.findBuyerById(ticketRequest.getBuyerId()); Musical musical = musicalTicketService.findMusicalById(ticketRequest.getMusicalId()); MusicalTicket musicalTicket = musicalTicketService.findTicketByMusical(musical); if (!musicalTicket.checkTicketingDate(ticketRequest.getTicketingDate())) { throw new RuntimeException("지금은 티켓팅 기간이 아닙니다."); } - musicalTicketService.buyTicket(buyer, musicalTicket); + musicalTicketService.buyTicket(buyer, musicalTicket.getId()); } } diff --git a/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java index 79f4a71..4f6ec43 100644 --- a/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java +++ b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java @@ -13,4 +13,11 @@ public class TicketRequest { private Integer buyerId; private Integer musicalId; private LocalDate TicketingDate; + + public static TicketRequest of(Integer buyerId, Integer musicalId, LocalDate TicketingDate) { + return TicketRequest.builder() + .buyerId(buyerId) + .musicalId(musicalId) + .TicketingDate(TicketingDate).build(); + } } diff --git a/src/main/java/com/week/zumgnmarket/fecade/dto/TicketResponse.java b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketResponse.java new file mode 100644 index 0000000..852fe27 --- /dev/null +++ b/src/main/java/com/week/zumgnmarket/fecade/dto/TicketResponse.java @@ -0,0 +1,22 @@ +package com.week.zumgnmarket.fecade.dto; + +import com.week.zumgnmarket.entity.Purchase; +import lombok.*; +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class TicketResponse { + + private Integer buyerId; + private String title; + private boolean isPurchase; + + public static TicketResponse of(Purchase purchase) { + return TicketResponse.builder() + .buyerId(purchase.getBuyerId()) + .title(purchase.getTitle()) + .isPurchase(true).build(); + } +} diff --git a/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java b/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java index 43b6c44..83e6c6b 100644 --- a/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java +++ b/src/main/java/com/week/zumgnmarket/service/MusicalTicketService.java @@ -3,39 +3,53 @@ import com.week.zumgnmarket.entity.Buyer; import com.week.zumgnmarket.entity.Musical; import com.week.zumgnmarket.entity.MusicalTicket; +import com.week.zumgnmarket.entity.Purchase; import com.week.zumgnmarket.repository.MusicalJpaRepository; +import com.week.zumgnmarket.repository.PurchaseJpaRepository; import com.week.zumgnmarket.repository.TicketJpaRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @Transactional +@Slf4j public class MusicalTicketService { private final TicketJpaRepository ticketJpaRepository; private final MusicalJpaRepository musicalJpaRepository; + private final PurchaseJpaRepository purchaseJpaRepository; + public MusicalTicket createTicket(MusicalTicket ticket) { return ticketJpaRepository.save(ticket); } - @Transactional(readOnly = true) + + public Musical createMusical(Musical musical) { + return musicalJpaRepository.save(musical); + } + public MusicalTicket findTicketByMusical(Musical musical) { return ticketJpaRepository.findByMusical(musical); } - public void buyTicket(Buyer buyer, MusicalTicket musicalTicket) { - Musical musical = musicalTicket.getMusical(); - if (!buyer.isExistMusicals(musical)) { - throw new RuntimeException("이미 해당 뮤지컬 티켓을 구매하셨습니다. "); - } - musicalTicket.decrease(); - buyer.updateMusicals(musicalTicket.getMusical()); + public MusicalTicket findTicketById(Integer ticketId) { + return ticketJpaRepository.findById(ticketId).orElseThrow(() -> new RuntimeException("존재하지 않는 티켓입니다")); } - @Transactional(readOnly = true) public Musical findMusicalById(Integer id) { return musicalJpaRepository.findById(id).orElseThrow(() -> new RuntimeException("존재하지 않는 뮤지컬 입니다. ")); } + + public Purchase buyTicket(Buyer buyer, Integer ticketId) { + MusicalTicket musicalTicket = findTicketById(ticketId); + Musical musical = findMusicalById(musicalTicket.getMusicalId()); + if (purchaseJpaRepository.existsByBuyerAndMusical(buyer, musical)) { + throw new RuntimeException("이미 해당 뮤지컬 티켓을 구매하셨습니다."); + } + musicalTicket.decrease(); + return purchaseJpaRepository.save(Purchase.of(buyer, musical)); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 740c9b9..5cd4959 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,10 +9,11 @@ spring: password: jpa: database: mysql - show-sql: true +# show-sql: true hibernate: ddl-auto: update logging: level: - org.hibernate.sql: debug - org.hibernate.type: trace \ No newline at end of file + root: error +# org.hibernate.sql: debug +# org.hibernate.type: trace \ No newline at end of file diff --git a/src/test/java/com/week/zumgnmarket/fecade/TicketFacadeTest.java b/src/test/java/com/week/zumgnmarket/fecade/TicketFacadeTest.java new file mode 100644 index 0000000..f9267b4 --- /dev/null +++ b/src/test/java/com/week/zumgnmarket/fecade/TicketFacadeTest.java @@ -0,0 +1,146 @@ +package com.week.zumgnmarket.fecade; + +import com.week.zumgnmarket.entity.Buyer; +import com.week.zumgnmarket.entity.Musical; +import com.week.zumgnmarket.entity.MusicalTicket; +import com.week.zumgnmarket.fecade.dto.TicketRequest; +import com.week.zumgnmarket.repository.BuyerJpaRepository; +import com.week.zumgnmarket.repository.MusicalJpaRepository; +import com.week.zumgnmarket.repository.PurchaseJpaRepository; +import com.week.zumgnmarket.repository.TicketJpaRepository; +import com.week.zumgnmarket.service.MusicalTicketService; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@SpringBootTest +class TicketFacadeTest { + + @Autowired + private TicketFacade ticketFacade; + + @Autowired + private MusicalTicketService musicalTicketService; + + @Autowired + private MusicalJpaRepository musicalJpaRepository; + + @Autowired + private TicketJpaRepository ticketJpaRepository; + + @Autowired + private BuyerJpaRepository buyerJpaRepository; + + @Autowired + private PurchaseJpaRepository purchaseJpaRepository; + + private static final Long TOTAL_AMOUNT = 100L; + private static final Long SOLD_OUT = 0L; + + private MusicalTicket musicalTicket_; + + private static List buyers = new ArrayList<>(); + + @BeforeEach + void 사용자_티켓_셋팅() { + + for (int i=0; i<150; i++) { + Buyer buyer = Buyer.of("김"+i+"씨"); + buyers.add(buyerJpaRepository.save(buyer)); + } + + Musical musical = Musical.of("캣츠", "1000만 돌파 !!"); + musicalTicketService.createMusical(musical); + MusicalTicket musicalTicket = MusicalTicket.of(musical, TOTAL_AMOUNT, LocalDate.now()); + musicalTicket_ = musicalTicketService.createTicket(musicalTicket); + } + + @AfterEach + void 사용자_티켓_초기화() { + for (int i = 0; i<150; i++) { + buyers.clear(); + } + purchaseJpaRepository.deleteAll(); + buyerJpaRepository.deleteAll(); + ticketJpaRepository.deleteAll(); + musicalJpaRepository.deleteAll(); + } + + @Test + @Order(1) + void 락0_150명이_티켓팅을_실행() throws InterruptedException { + int people = 150; + CountDownLatch countDownLatch = new CountDownLatch(people); + List threads = buyers.stream() + .map(buyer -> { + TicketRequest ticketRequest = TicketRequest.of(buyer.getId(), musicalTicket_.getMusicalId(), LocalDate.now()); + return new Thread(new Buying(ticketRequest, countDownLatch)); + }) + .limit(people).collect(Collectors.toList()); + threads.forEach(Thread::start); + countDownLatch.await(); + MusicalTicket musicalTicket = musicalTicketService.findTicketById(musicalTicket_.getId()); + assertEquals(musicalTicket.getTicketCount(), SOLD_OUT); + } + + @Test + @Order(1) + void 락X_150명이_티켓팅을_실행() throws InterruptedException { + int people = 150; + CountDownLatch countDownLatch = new CountDownLatch(people); + List threads = buyers.stream() + .map(buyer -> { + TicketRequest ticketRequest = TicketRequest.of(buyer.getId(), musicalTicket_.getMusicalId(), LocalDate.now()); + return new Thread(new BuyingNoLock(ticketRequest, countDownLatch)); + }) + .limit(people).collect(Collectors.toList()); + threads.forEach(Thread::start); + countDownLatch.await(); + MusicalTicket musicalTicket = musicalTicketService.findTicketById(musicalTicket_.getId()); + assertNotEquals(musicalTicket.getTicketCount(), SOLD_OUT); + } + + private class Buying implements Runnable { + + private TicketRequest ticketRequest; + private CountDownLatch countDownLatch; + + public Buying(TicketRequest ticketRequest, CountDownLatch countDownLatch) { + this.ticketRequest = ticketRequest; + this.countDownLatch = countDownLatch; + } + + @Override + public void run() { + ticketFacade.buy(ticketRequest); + countDownLatch.countDown(); + } + } + + private class BuyingNoLock implements Runnable { + + private TicketRequest ticketRequest; + private CountDownLatch countDownLatch; + + public BuyingNoLock(TicketRequest ticketRequest, CountDownLatch countDownLatch) { + this.ticketRequest = ticketRequest; + this.countDownLatch = countDownLatch; + } + + @Override + public void run() { + ticketFacade.buyNoLock(ticketRequest); + countDownLatch.countDown(); + } + } +} \ No newline at end of file