-
Notifications
You must be signed in to change notification settings - Fork 5
2주차 : 줌터파크 과제 제출합니다. #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| version: '3.9' | ||
| services: | ||
| redis: | ||
| image: 'redis:alpine' | ||
| hostname: redis | ||
| container_name: redis-dailyProject | ||
| ports: | ||
| - '6379:6379' | ||
|
|
||
| volumes: | ||
| redis: | ||
| driver: local |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.week.zumgnmarket.controller; | ||
|
|
||
| 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; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/tickets") | ||
| @RequiredArgsConstructor | ||
| public class TicketController { | ||
|
|
||
| private final TicketFacade ticketFacade; | ||
|
|
||
| @GetMapping("/buy") | ||
| public ResponseEntity<TicketResponse> buy(@RequestBody TicketRequest ticketRequest) { | ||
| return ResponseEntity.ok(ticketFacade.buy(ticketRequest)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.week.zumgnmarket.entity; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| import javax.persistence.*; | ||
|
|
||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Getter | ||
| @Builder | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클래스에 |
||
| @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; | ||
|
|
||
| public static Buyer of(String nickName) { | ||
| return Buyer.builder() | ||
| .nickName(nickName).build(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.week.zumgnmarket.entity; | ||
|
|
||
| import lombok.*; | ||
|
|
||
| import javax.persistence.*; | ||
|
|
||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Getter | ||
| @Builder | ||
| @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; | ||
|
|
||
| public static Musical of(String title, String description) { | ||
| return Musical.builder() | ||
| .title(title) | ||
| .description(description) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.week.zumgnmarket.entity; | ||
|
|
||
| import lombok.*; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| import javax.persistence.*; | ||
| import java.time.LocalDate; | ||
|
|
||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Getter | ||
| @Slf4j | ||
| @Builder | ||
| @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 static MusicalTicket of(Musical musical, Long ticketCount, LocalDate ticketingDate) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로젝트를 보면 정적 팩토리 메소드로 객체를 생성하는 방법과, 빌더 패턴으로 객체를 생성하는 방법 |
||
| return 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("오늘자 티켓팅이 마감되었습니다 (재고 부족)"); - 테스트 위해 주석 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 불필요한 코드와 주석은 과감하게 삭제하는 것도 좋을 것 같아요~ |
||
| log.error("진행중인 사람 : {}, 메세지 : 티켓팅이 마감되었습니다(재고 부족 - {} 개) ", thread, this.ticketCount); | ||
| return; | ||
| } | ||
| this.ticketCount = this.ticketCount - 1; | ||
| log.error("진행중인 사람 : {}, 메세지 : 티켓팅을 성공하셨습니다(남은 티켓 갯수 - {} 개) ", thread, this.ticketCount); | ||
| } | ||
|
|
||
| public boolean checkTicketingDate(LocalDate localDate) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프레디케이트 메소드는 자바 메소드 규약에 맞게 |
||
| return this.ticketingDate.equals(localDate); | ||
| } | ||
|
|
||
| public Integer getMusicalId() { | ||
| return this.musical.getId(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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_idx") | ||
| 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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| 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.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())) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 조건문도 명확하게 메소드로 추상화 할 수 있을 것 같습니다. |
||
| throw new RuntimeException("지금은 티켓팅 기간이 아닙니다."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| RLock lock = redissonClient.getLock(musical.getTitle() + ":" + ticketRequest.getTicketingDate()); | ||
| try { | ||
| if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 락의 획득시도 시간, 락 점유 시간을 선택하신 기준이 있으신가요? 저도 동시성 이슈는 부족해서 |
||
| return 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 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.getId()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 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; | ||
|
|
||
| public static TicketRequest of(Integer buyerId, Integer musicalId, LocalDate TicketingDate) { | ||
| return TicketRequest.builder() | ||
| .buyerId(buyerId) | ||
| .musicalId(musicalId) | ||
| .TicketingDate(TicketingDate).build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.week.zumgnmarket.fecade.dto; | ||
|
|
||
| import com.week.zumgnmarket.entity.Purchase; | ||
| import lombok.*; | ||
| @Builder | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 띄어쓰기가 조금 이상한 것 같아요 |
||
| @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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Buyer, Integer> { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Musical, Integer> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Purchase, Integer> { | ||
| boolean existsByBuyerAndMusical(Buyer buyer, Musical musical); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사람마다 견해가 다르지만 import 문에서 와일드카드는 지양하는 편입니다.
참고해서 링크 드릴게요
https://dev.to/kingori/import-5531