From a25bf5be56f2ceb2db539924a2a0f683d9544982 Mon Sep 17 00:00:00 2001 From: yuseonjun Date: Fri, 15 Mar 2024 11:33:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20mock=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doochul/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/doochul/build.gradle b/doochul/build.gradle index 81f9f50..d65958d 100644 --- a/doochul/build.gradle +++ b/doochul/build.gradle @@ -34,6 +34,7 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-core:3.12.4' } tasks.named('test') { From 9ac330ed7e36818e1f21ee55fd22bac04073fa88 Mon Sep 17 00:00:00 2001 From: yuseonjun Date: Fri, 15 Mar 2024 12:49:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EC=88=98=EA=B0=95=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doochul/build.gradle | 2 + .../application/MemberShipService.java | 28 +++++++++++ .../doochul/domain/membership/MemberShip.java | 11 +++++ .../org/doochul/domain/product/Product.java | 8 +++ .../java/org/doochul/domain/user/User.java | 9 ++++ .../org/doochul/ui/MemberShipController.java | 17 +++++++ .../application/MemberShipServiceTest.java | 49 +++++++++++++++++++ 7 files changed, 124 insertions(+) create mode 100644 doochul/src/main/java/org/doochul/application/MemberShipService.java create mode 100644 doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java diff --git a/doochul/build.gradle b/doochul/build.gradle index d65958d..da378d5 100644 --- a/doochul/build.gradle +++ b/doochul/build.gradle @@ -25,11 +25,13 @@ 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 'com.google.firebase:firebase-admin:9.2.0' runtimeOnly 'mysql:mysql-connector-java' + runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' 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..3320f44 --- /dev/null +++ b/doochul/src/main/java/org/doochul/application/MemberShipService.java @@ -0,0 +1,28 @@ +package org.doochul.application; + +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; + + public MemberShip save(Long userId, Long productId) { + Product product = productRepository.findById(productId).orElseThrow(); + User user = userRepository.findById(userId).orElseThrow(); + + return memberShipRepository.save(MemberShip.of(user, product, product.getCount())); + } +} 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 382e41b..f70032a 100644 --- a/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java +++ b/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java @@ -46,4 +46,15 @@ private void validateMinRemainingCount() { throw new IllegalArgumentException("안돼"); } } + + private MemberShip(Long id, User student, Product product, Integer remainingCount) { + this.id = id; + this.student = student; + this.product = product; + this.remainingCount = remainingCount; + } + + public static MemberShip of(User student, Product product, Integer remainingCount) { + return new MemberShip(null, student, product, remainingCount); + } } 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 f9ba1b5..69e2bb0 100644 --- a/doochul/src/main/java/org/doochul/domain/product/Product.java +++ b/doochul/src/main/java/org/doochul/domain/product/Product.java @@ -33,4 +33,12 @@ public class Product extends BaseEntity { private User teacher; private Integer count; + + public Product(Long id, String name, ProductType type, User teacher, Integer count) { + this.id = id; + this.name = name; + this.type = type; + this.teacher = teacher; + this.count = count; + } } 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 39391b8..93fbd85 100644 --- a/doochul/src/main/java/org/doochul/domain/user/User.java +++ b/doochul/src/main/java/org/doochul/domain/user/User.java @@ -36,4 +36,13 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Identity identity; + + public User(Long id, String name, String deviceToken, String passWord, Gender gender, Identity identity) { + this.id = id; + this.name = name; + this.deviceToken = deviceToken; + this.passWord = passWord; + this.gender = gender; + this.identity = identity; + } } diff --git a/doochul/src/main/java/org/doochul/ui/MemberShipController.java b/doochul/src/main/java/org/doochul/ui/MemberShipController.java index 69b77ad..bee64af 100644 --- a/doochul/src/main/java/org/doochul/ui/MemberShipController.java +++ b/doochul/src/main/java/org/doochul/ui/MemberShipController.java @@ -1,9 +1,26 @@ package org.doochul.ui; +import lombok.RequiredArgsConstructor; +import org.doochul.application.MemberShipService; +import org.doochul.domain.membership.MemberShip; +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 MemberShip apply( + @AuthenticationPrincipal final Long userId, + @PathVariable Long productId) { + + return memberShipService.save(userId, productId); + } } 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..c2bdd89 --- /dev/null +++ b/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java @@ -0,0 +1,49 @@ +package org.doochul.application; + +import static org.assertj.core.api.Assertions.assertThat; + +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; + +@SpringBootTest +public class MemberShipServiceTest { + + @Autowired + private MemberShipRepository memberShipRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private UserRepository userRepository; + + @Test + void memberShip에_저장한_userId는_User에_저장한_userId와_같아야한다() { + // 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); + + // When + MemberShip savedMemberShip = new MemberShipService(memberShipRepository, productRepository, userRepository) + .save(user.getId(), product.getId()); + + // Then + assertThat(user.getId()).isEqualTo(savedMemberShip.getStudent().getId()); + } +} From d3d6a092a3e0c98e3b2e68b723a0aed57b7793cd Mon Sep 17 00:00:00 2001 From: yuseonjun Date: Fri, 15 Mar 2024 14:21:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20Redis=EC=9D=98=20setNX=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=9C=20=EC=88=98=EA=B0=95=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EB=9D=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/MemberShipService.java | 16 +++++-- .../org/doochul/application/RedisService.java | 1 - .../doochul/domain/membership/MemberShip.java | 2 +- .../org/doochul/ui/MemberShipController.java | 2 +- .../application/MemberShipServiceTest.java | 44 +++++++++++++++---- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/doochul/src/main/java/org/doochul/application/MemberShipService.java b/doochul/src/main/java/org/doochul/application/MemberShipService.java index 3320f44..10ca112 100644 --- a/doochul/src/main/java/org/doochul/application/MemberShipService.java +++ b/doochul/src/main/java/org/doochul/application/MemberShipService.java @@ -1,5 +1,6 @@ package org.doochul.application; +import java.time.Duration; import lombok.RequiredArgsConstructor; import org.doochul.domain.membership.MemberShip; import org.doochul.domain.membership.MemberShipRepository; @@ -18,11 +19,18 @@ public class MemberShipService { private final MemberShipRepository memberShipRepository; private final ProductRepository productRepository; private final UserRepository userRepository; + private final RedisService redisService; - public MemberShip save(Long userId, Long productId) { - Product product = productRepository.findById(productId).orElseThrow(); - User user = userRepository.findById(userId).orElseThrow(); + public Long save(final Long userId, final Long productId) { + final Product product = productRepository.findById(productId).orElseThrow(); + final User user = userRepository.findById(userId).orElseThrow(); - return memberShipRepository.save(MemberShip.of(user, product, product.getCount())); + 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/application/RedisService.java b/doochul/src/main/java/org/doochul/application/RedisService.java index 5bd1be3..5c37dc1 100644 --- a/doochul/src/main/java/org/doochul/application/RedisService.java +++ b/doochul/src/main/java/org/doochul/application/RedisService.java @@ -18,7 +18,6 @@ public void delete(final String key) { redisTemplate.delete(key); } - public boolean setNX(final String key, final String value, final Duration duration) { return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, duration)); } 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 f70032a..94113c7 100644 --- a/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java +++ b/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java @@ -47,7 +47,7 @@ private void validateMinRemainingCount() { } } - private MemberShip(Long id, User student, Product product, Integer remainingCount) { + public MemberShip(Long id, User student, Product product, Integer remainingCount) { this.id = id; this.student = student; this.product = product; diff --git a/doochul/src/main/java/org/doochul/ui/MemberShipController.java b/doochul/src/main/java/org/doochul/ui/MemberShipController.java index bee64af..c9b228e 100644 --- a/doochul/src/main/java/org/doochul/ui/MemberShipController.java +++ b/doochul/src/main/java/org/doochul/ui/MemberShipController.java @@ -17,7 +17,7 @@ public class MemberShipController { private final MemberShipService memberShipService; @PostMapping("/apply/{productId}") - public MemberShip apply( + public Long apply( @AuthenticationPrincipal final Long userId, @PathVariable Long productId) { diff --git a/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java b/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java index c2bdd89..9c6e47d 100644 --- a/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java +++ b/doochul/src/test/java/org/doochul/application/MemberShipServiceTest.java @@ -1,7 +1,14 @@ package org.doochul.application; -import static org.assertj.core.api.Assertions.assertThat; +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; @@ -14,11 +21,12 @@ 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 { - @Autowired + @MockBean private MemberShipRepository memberShipRepository; @Autowired @@ -27,8 +35,11 @@ public class MemberShipServiceTest { @Autowired private UserRepository userRepository; + @Autowired + private RedisService redisService; + @Test - void memberShip에_저장한_userId는_User에_저장한_userId와_같아야한다() { + 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); @@ -39,11 +50,28 @@ public class MemberShipServiceTest { Product product = new Product(1L, "페이커", ProductType.LOL, teacher, 10); productRepository.save(product); - // When - MemberShip savedMemberShip = new MemberShipService(memberShipRepository, productRepository, userRepository) - .save(user.getId(), product.getId()); + 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 - assertThat(user.getId()).isEqualTo(savedMemberShip.getStudent().getId()); + then(memberShipRepository).should(times(1)).save(any()); } } From 9fde205f32c3d88d697b3fb0dcac99fc009679ac Mon Sep 17 00:00:00 2001 From: yuseonjun Date: Sun, 31 Mar 2024 16:45:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20final=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doochul/domain/membership/MemberShip.java | 22 +++++++++---------- .../org/doochul/domain/product/Product.java | 2 +- .../java/org/doochul/domain/user/User.java | 8 ++++++- .../org/doochul/ui/MemberShipController.java | 11 +++++----- 4 files changed, 24 insertions(+), 19 deletions(-) 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 94113c7..d725b6d 100644 --- a/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java +++ b/doochul/src/main/java/org/doochul/domain/membership/MemberShip.java @@ -32,6 +32,17 @@ public class MemberShip extends BaseEntity { private 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; @@ -46,15 +57,4 @@ private void validateMinRemainingCount() { throw new IllegalArgumentException("안돼"); } } - - public MemberShip(Long id, User student, Product product, Integer remainingCount) { - this.id = id; - this.student = student; - this.product = product; - this.remainingCount = remainingCount; - } - - public static MemberShip of(User student, Product product, Integer remainingCount) { - return new MemberShip(null, student, product, remainingCount); - } } 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 69e2bb0..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,7 @@ public class Product extends BaseEntity { private Integer count; - public Product(Long id, String name, ProductType type, User teacher, 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; 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 93fbd85..5637dc8 100644 --- a/doochul/src/main/java/org/doochul/domain/user/User.java +++ b/doochul/src/main/java/org/doochul/domain/user/User.java @@ -37,7 +37,13 @@ public class User extends BaseEntity { @Enumerated(EnumType.STRING) private Identity identity; - public User(Long id, String name, String deviceToken, String passWord, Gender gender, 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; diff --git a/doochul/src/main/java/org/doochul/ui/MemberShipController.java b/doochul/src/main/java/org/doochul/ui/MemberShipController.java index c9b228e..c29cea9 100644 --- a/doochul/src/main/java/org/doochul/ui/MemberShipController.java +++ b/doochul/src/main/java/org/doochul/ui/MemberShipController.java @@ -1,8 +1,9 @@ package org.doochul.ui; +import java.net.URI; import lombok.RequiredArgsConstructor; import org.doochul.application.MemberShipService; -import org.doochul.domain.membership.MemberShip; +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; @@ -17,10 +18,8 @@ public class MemberShipController { private final MemberShipService memberShipService; @PostMapping("/apply/{productId}") - public Long apply( - @AuthenticationPrincipal final Long userId, - @PathVariable Long productId) { - - return memberShipService.save(userId, 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(); } }