diff --git a/doochul/build.gradle b/doochul/build.gradle index bd981b9..1df13b2 100644 --- a/doochul/build.gradle +++ b/doochul/build.gradle @@ -25,16 +25,17 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'com.google.firebase:firebase-admin:9.2.0' - runtimeOnly 'mysql:mysql-connector-java:8.0.28' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.batch:spring-batch-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-core:3.12.4' } tasks.named('test') { diff --git a/doochul/src/main/java/org/doochul/application/MemberShipService.java b/doochul/src/main/java/org/doochul/application/MemberShipService.java new file mode 100644 index 0000000..10ca112 --- /dev/null +++ b/doochul/src/main/java/org/doochul/application/MemberShipService.java @@ -0,0 +1,36 @@ +package org.doochul.application; + +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import org.doochul.domain.membership.MemberShip; +import org.doochul.domain.membership.MemberShipRepository; +import org.doochul.domain.product.Product; +import org.doochul.domain.product.ProductRepository; +import org.doochul.domain.user.User; +import org.doochul.domain.user.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberShipService { + + private final MemberShipRepository memberShipRepository; + private final ProductRepository productRepository; + private final UserRepository userRepository; + private final RedisService redisService; + + public Long save(final Long userId, final Long productId) { + final Product product = productRepository.findById(productId).orElseThrow(); + final User user = userRepository.findById(userId).orElseThrow(); + + final String key = Long.toString(userId); + if (redisService.setNX(key, "apply", Duration.ofSeconds(5))) { + final Long id = memberShipRepository.save(MemberShip.of(user, product, product.getCount())).getId(); + redisService.delete(key); + return id; + } + throw new IllegalArgumentException(); + } +} diff --git a/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java b/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java index 84d6d74..d725b6d 100644 --- a/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java +++ b/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java @@ -23,7 +23,7 @@ public class MemberShip extends BaseEntity { private Long id; @ManyToOne - @JoinColumn(name = "student_id") + @JoinColumn(name = "user_id") private User student; @ManyToOne @@ -32,12 +32,17 @@ public class MemberShip extends BaseEntity { private Integer remainingCount; - public MemberShip(final User student, final Product product, final Integer remainingCount) { + public MemberShip(final Long id, final User student, final Product product, final Integer remainingCount) { + this.id = id; this.student = student; this.product = product; this.remainingCount = remainingCount; } + public static MemberShip of(final User student, final Product product, final Integer remainingCount) { + return new MemberShip(null, student, product, remainingCount); + } + public void decreasedCount() { validateMinRemainingCount(); remainingCount -= 1; diff --git a/doochul/src/main/java/org/doochul/domain/product/Product.java b/doochul/src/main/java/org/doochul/domain/product/Product.java index e43cd98..75b0453 100644 --- a/doochul/src/main/java/org/doochul/domain/product/Product.java +++ b/doochul/src/main/java/org/doochul/domain/product/Product.java @@ -34,7 +34,8 @@ public class Product extends BaseEntity { private Integer count; - public Product(final String name, final ProductType type, final User teacher, final Integer count) { + public Product(final Long id, final String name, final ProductType type, final User teacher, final Integer count) { + this.id = id; this.name = name; this.type = type; this.teacher = teacher; diff --git a/doochul/src/main/java/org/doochul/domain/user/User.java b/doochul/src/main/java/org/doochul/domain/user/User.java index 0af8242..5637dc8 100644 --- a/doochul/src/main/java/org/doochul/domain/user/User.java +++ b/doochul/src/main/java/org/doochul/domain/user/User.java @@ -11,6 +11,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import org.doochul.domain.BaseEntity; @Entity @@ -36,13 +37,14 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Identity identity; - public User( - final String name, - final String deviceToken, - final String passWord, - final Gender gender, - final Identity identity + public User(final Long id, + final String name, + final String deviceToken, + final String passWord, + final Gender gender, + final Identity identity ) { + this.id = id; this.name = name; this.deviceToken = deviceToken; this.passWord = passWord; diff --git a/doochul/src/main/java/org/doochul/ui/MemberShipController.java b/doochul/src/main/java/org/doochul/ui/MemberShipController.java index 69b77ad..c29cea9 100644 --- a/doochul/src/main/java/org/doochul/ui/MemberShipController.java +++ b/doochul/src/main/java/org/doochul/ui/MemberShipController.java @@ -1,9 +1,25 @@ package org.doochul.ui; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.doochul.application.MemberShipService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController +@RequiredArgsConstructor +@RequestMapping("/memberShip") public class MemberShipController { + private final MemberShipService memberShipService; + @PostMapping("/apply/{productId}") + public ResponseEntity apply(@AuthenticationPrincipal final Long userId, @PathVariable final Long productId) { + Long memberShipId = memberShipService.save(userId, productId); + return ResponseEntity.created(URI.create("/memberShips" + memberShipId)).build(); + } } diff --git a/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java b/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java new file mode 100644 index 0000000..9c6e47d --- /dev/null +++ b/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java @@ -0,0 +1,77 @@ +package org.doochul.application; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.doochul.domain.membership.MemberShip; +import org.doochul.domain.membership.MemberShipRepository; +import org.doochul.domain.product.Product; +import org.doochul.domain.product.ProductRepository; +import org.doochul.domain.product.ProductType; +import org.doochul.domain.user.Gender; +import org.doochul.domain.user.Identity; +import org.doochul.domain.user.User; +import org.doochul.domain.user.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@SpringBootTest +public class MemberShipServiceTest { + + @MockBean + private MemberShipRepository memberShipRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RedisService redisService; + + @Test + void memberShip_save() throws InterruptedException { + // Given + User user = new User(2L, "JEON", "deviceToken1234", "password1234", Gender.MEN, Identity.GENERAL); + User teacher = new User(1L, "Faker", "deviceToken123", "password123", Gender.MEN, Identity.TEACHER); + + userRepository.save(user); + userRepository.save(teacher); + + Product product = new Product(1L, "페이커", ProductType.LOL, teacher, 10); + productRepository.save(product); + + ExecutorService executorService = Executors.newFixedThreadPool(5); + CountDownLatch latch = new CountDownLatch(5); + + given(memberShipRepository.save(any())).willReturn(new MemberShip(1L, user, product, 10)); + + for (int i = 0; i < 5; i++) { + executorService.submit(() -> { + try { + final String key = Long.toString(user.getId()); + if (redisService.setNX(key, "apply", Duration.ofSeconds(5))) { + memberShipRepository.save(MemberShip.of(user, product, product.getCount())); + redisService.delete(key); + } + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + then(memberShipRepository).should(times(1)).save(any()); + } +}