Skip to content
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

[release] v1.2.1 릴리즈 #159

Merged
merged 8 commits into from
Apr 10, 2024
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ out/
.DS_Store

### Yml ###
doorip-api/src/main/resources/application.yml
doorip-api/src/main/resources/application.yml
doorip-api/src/test/resources/application.yml
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ subprojects {

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
Expand Down
1 change: 1 addition & 0 deletions doorip-api/src/main/java/org/doorip/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public abstract class Constants {
public static final int TODO_OWNER_POSITION = 0;
public static final int START_STYLE_POS = 0;
public static final int END_STYLE_POS = 5;
public static final String SIGN_UP_LOCK = "signup";
}
32 changes: 14 additions & 18 deletions doorip-api/src/main/java/org/doorip/user/api/UserApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

import lombok.RequiredArgsConstructor;
import org.doorip.auth.UserId;
import org.doorip.common.BaseResponse;
import org.doorip.common.ApiResponseUtil;
import org.doorip.common.BaseResponse;
import org.doorip.message.SuccessMessage;
import org.doorip.user.dto.request.ResultUpdateRequest;
import org.doorip.user.dto.request.UserReissueRequest;
import org.doorip.user.dto.request.UserSignInRequest;
import org.doorip.user.dto.request.UserSignUpRequest;
import org.doorip.user.dto.request.ProfileUpdateRequest;
import org.doorip.user.dto.request.*;
import org.doorip.user.dto.response.ProfileGetResponse;
import org.doorip.user.dto.response.UserSignUpResponse;
import org.doorip.user.dto.response.UserSignInResponse;
import org.doorip.user.service.UserService;
import org.doorip.user.dto.response.UserSignUpResponse;
import org.doorip.user.facade.UserFacade;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
Expand All @@ -24,72 +20,72 @@
@RequestMapping("/api/users")
@Controller
public class UserApiController implements UserApi {
private final UserService userService;
private final UserFacade userFacade;

@GetMapping("/splash")
@Override
public ResponseEntity<BaseResponse<?>> splash(@UserId final Long userId) {
userService.splash(userId);
userFacade.splash(userId);
return ApiResponseUtil.success(SuccessMessage.OK);
}

@PostMapping("/signin")
@Override
public ResponseEntity<BaseResponse<?>> signIn(@RequestHeader(AUTHORIZATION) final String token,
@RequestBody final UserSignInRequest request) {
final UserSignInResponse response = userService.signIn(token, request);
final UserSignInResponse response = userFacade.signIn(token, request);
return ApiResponseUtil.success(SuccessMessage.OK, response);
}

@PostMapping("/signup")
@Override
public ResponseEntity<BaseResponse<?>> signUp(@RequestHeader(AUTHORIZATION) final String token,
@RequestBody final UserSignUpRequest request) {
final UserSignUpResponse response = userService.signUp(token, request);
final UserSignUpResponse response = userFacade.signUp(token, request);
return ApiResponseUtil.success(SuccessMessage.CREATED, response);
}

@PatchMapping("/signout")
@Override
public ResponseEntity<BaseResponse<?>> signOut(@UserId final Long userId) {
userService.signOut(userId);
userFacade.signOut(userId);
return ApiResponseUtil.success(SuccessMessage.OK);
}

@DeleteMapping("/withdraw")
@Override
public ResponseEntity<BaseResponse<?>> withdraw(@UserId final Long userId) {
userService.withdraw(userId);
userFacade.withdraw(userId);
return ApiResponseUtil.success(SuccessMessage.OK);
}

@PostMapping("/reissue")
@Override
public ResponseEntity<BaseResponse<?>> reissue(@RequestHeader(AUTHORIZATION) final String refreshtoken,
@RequestBody final UserReissueRequest request) {
final UserSignUpResponse response = userService.reissue(refreshtoken, request);
final UserSignUpResponse response = userFacade.reissue(refreshtoken, request);
return ApiResponseUtil.success(SuccessMessage.OK, response);
}

@GetMapping("/profile")
@Override
public ResponseEntity<BaseResponse<?>> getProfile(@UserId final Long userId) {
final ProfileGetResponse response = userService.getProfile(userId);
final ProfileGetResponse response = userFacade.getProfile(userId);
return ApiResponseUtil.success(SuccessMessage.OK, response);
}

@PatchMapping("/test")
@Override
public ResponseEntity<BaseResponse<?>> updateResult(@UserId final Long userId,
@RequestBody final ResultUpdateRequest request) {
userService.updateResult(userId, request);
userFacade.updateResult(userId, request);
return ApiResponseUtil.success(SuccessMessage.OK);
}

@PatchMapping("/profile")
public ResponseEntity<BaseResponse<?>> updateProfile(@UserId final Long userId,
@RequestBody final ProfileUpdateRequest request) {
userService.updateProfile(userId, request);
userFacade.updateProfile(userId, request);
return ApiResponseUtil.success(SuccessMessage.OK);
}
}
65 changes: 65 additions & 0 deletions doorip-api/src/main/java/org/doorip/user/facade/UserFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.doorip.user.facade;

import lombok.RequiredArgsConstructor;
import org.doorip.common.Constants;
import org.doorip.user.dto.request.*;
import org.doorip.user.dto.response.ProfileGetResponse;
import org.doorip.user.dto.response.UserSignInResponse;
import org.doorip.user.dto.response.UserSignUpResponse;
import org.doorip.user.repository.LettuceLockRepository;
import org.doorip.user.service.UserService;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class UserFacade {
private final UserService userService;
private final LettuceLockRepository lettuceLockRepository;

public void splash(Long userId) {
userService.splash(userId);
}

public UserSignInResponse signIn(String token, UserSignInRequest request) {
return userService.signIn(token, request);
}

public UserSignUpResponse signUp(String token, UserSignUpRequest request) {
while (!lettuceLockRepository.lock(token, Constants.SIGN_UP_LOCK)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
return userService.signUp(token, request);
} finally {
lettuceLockRepository.unlock(token);
}
}

public void signOut(Long userId) {
userService.signOut(userId);
}

public void withdraw(Long userId) {
userService.withdraw(userId);
}

public UserSignUpResponse reissue(String refreshToken, UserReissueRequest request) {
return userService.reissue(refreshToken, request);
}

public ProfileGetResponse getProfile(Long userId) {
return userService.getProfile(userId);
}

public void updateResult(Long userId, ResultUpdateRequest request) {
userService.updateResult(userId, request);
}

public void updateProfile(Long userId, ProfileUpdateRequest request) {
userService.updateProfile(userId, request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.doorip.user.facade;

import lombok.extern.slf4j.Slf4j;
import org.doorip.exception.ConflictException;
import org.doorip.exception.EntityNotFoundException;
import org.doorip.message.ErrorMessage;
import org.doorip.user.dto.request.UserSignInRequest;
import org.doorip.user.dto.request.UserSignUpRequest;
import org.doorip.user.dto.response.UserSignInResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Slf4j
@SpringBootTest
class UserFacadeTest {
@Autowired
UserFacade userFacade;
@Value("${oauth.kakao.test}")
String accessToken;

@AfterEach
void afterEach() {
UserSignInRequest request = new UserSignInRequest("kakao");
try {
UserSignInResponse response = userFacade.signIn(accessToken, request);
userFacade.withdraw(response.userId());
} catch (EntityNotFoundException e) {
log.error("After Each Error: ", e);
throw e;
}
}

@DisplayName("동일한 회원의 회원가입 요청이 동시에 여러 개 들어오는 경우 정상적으로 회원가입된다.")
@Test
void 동일한_회원의_회원가입_요청이_동시에_여러_개_들어오는_경우_정상적으로_회원가입된다() throws InterruptedException {
// given
int threadCount = 20;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
UserSignUpRequest signUpRequest = new UserSignUpRequest("개발자", "동시성 테스트", "kakao");
UserSignInRequest signInRequest = new UserSignInRequest("kakao");

// when
IntStream.range(0, threadCount)
.forEach(i ->
executorService.submit(() -> {
try {
userFacade.signUp(accessToken, signUpRequest);
} catch (ConflictException e) {
log.error("Sub Task Thread Error: ", e);
throw e;
} finally {
latch.countDown();
}
}));
latch.await();

// then
assertThatThrownBy(() -> userFacade.signUp(accessToken, signUpRequest))
.isInstanceOf(ConflictException.class)
.hasMessage(ErrorMessage.DUPLICATE_USER.getMessage());
UserSignInResponse response = userFacade.signIn(accessToken, signInRequest);
assertThat(response.userId()).isNotNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.doorip.user.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.time.Duration;

@RequiredArgsConstructor
@Repository
public class LettuceLockRepository {
private final RedisTemplate<String, String> redisTemplate;

public Boolean lock(String token, String lockType) {
return redisTemplate
.opsForValue()
.setIfAbsent(token, lockType, Duration.ofSeconds(3L));
}

public void unlock(String token) {
redisTemplate.delete(token);
}
}
Loading