Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yml
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
21 changes: 21 additions & 0 deletions src/main/java/com/week/zumgnmarket/config/QueryDslConfig.java
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);
}
}

28 changes: 28 additions & 0 deletions src/main/java/com/week/zumgnmarket/config/RedissonConfig.java
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));
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/week/zumgnmarket/entity/Buyer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.week.zumgnmarket.entity;

import lombok.*;
Copy link
Contributor

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


import javax.persistence.*;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스에 @Builder 어노테이션을 추가하신 이유를 알 수 있을까요?

@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();
}

}
32 changes: 32 additions & 0 deletions src/main/java/com/week/zumgnmarket/entity/Musical.java
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();
}
}
59 changes: 59 additions & 0 deletions src/main/java/com/week/zumgnmarket/entity/MusicalTicket.java
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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("오늘자 티켓팅이 마감되었습니다 (재고 부족)"); - 테스트 위해 주석
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 코드와 주석은 과감하게 삭제하는 것도 좋을 것 같아요~
어차피 Git 에서 버전 관리를 해주고 있으니까요

log.error("진행중인 사람 : {}, 메세지 : 티켓팅이 마감되었습니다(재고 부족 - {} 개) ", thread, this.ticketCount);
return;
}
this.ticketCount = this.ticketCount - 1;
log.error("진행중인 사람 : {}, 메세지 : 티켓팅을 성공하셨습니다(남은 티켓 갯수 - {} 개) ", thread, this.ticketCount);
}

public boolean checkTicketingDate(LocalDate localDate) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프레디케이트 메소드는 자바 메소드 규약에 맞게 is~ , has~ 로 통일하는 방법도 있습니다~!

return this.ticketingDate.equals(localDate);
}

public Integer getMusicalId() {
return this.musical.getId();
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/week/zumgnmarket/entity/Purchase.java
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();
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/week/zumgnmarket/fecade/TicketFacade.java
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())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 조건문도 명확하게 메소드로 추상화 할 수 있을 것 같습니다.

throw new RuntimeException("지금은 티켓팅 기간이 아닙니다.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeException 으로 예외를 처리하는 것보다는 티켓팅 기간이 아니라는 적절한 예외로 포장하는 것이 좋아보여요~

}
RLock lock = redissonClient.getLock(musical.getTitle() + ":" + ticketRequest.getTicketingDate());
try {
if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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());
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/week/zumgnmarket/fecade/dto/TicketRequest.java
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();
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/week/zumgnmarket/fecade/dto/TicketResponse.java
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
Loading