이번에는 SignService를 이용하여 회원가입과 로그인의 웹 계층 api를 개발해보겠습니다.
Dependency 추가
build.gradle에 객체 검증을 위한 validation을 추가해 줍니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' // <- 추가
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
API 응답 생성
API 요청에 대한 응답 객체를 생성해 보겠습니다.
Response.java
@JsonInclude(JsonInclude.Include.NON_NULL) // 필드에 null 값을 가지고 있을 때 JSON 출력에서 해당 필드를 생략
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드를 포함한 생성자를 생성, 접근 권한이 private 이기 때문에 클래스 외부에서는 호출 불가
@Getter
public class Response {
private boolean success;
private int code;
private Result result;
public static Response success() { // 회원가입이 성공하면 성공 여부만 보여준다.
return new Response(true, 0, null);
}
public static <T> Response success(T data) { // 로그인이 성공하면 성공 데이터를 보여준다.
return new Response(true, 0, new Success<>(data));
}
public static Response failure(int code, String msg) { // 실패시에 실패 코드와 메세지를 보여준다.
System.out.println("msg = " + msg);
return new Response(false, code, new Failure(msg));
}
}
Response 클래스는 api 호출시에 응답하는 객체 입니다. 간단하게 정리를 해보자면
- success() : 회원 가입이 성공시에 응답
- success(T data) : 로그인이 성공시에 응답
- failure(int code, String msg) : 실패하면 실패 코드와 메세지를 가지고 응답
다음으로 Response의 필드 중 하나인 Result를 살펴보겠습니다.
Result.java
public interface Result {
}
그리고 Result 인터페이스를 상속받는 Success와 Failure입니다.
Success.java
@AllArgsConstructor
@Getter
public class Success<T> implements Result {
private T data;
}
Failure.java
@AllArgsConstructor
@Getter
public class Failure implements Result{
private String msg;
}
Sign Controller 생성
@RestController // @Controller + @ResponseBody
@RequiredArgsConstructor // 클래스에서 선언된 final 필드를 가지고 생성자를 만들어줌
public class SignController {
private final SignService signService;
@PostMapping("/api/sign-up") //HTTP POST 요청을 처리하는 핸들러 메소드를 지정
@ResponseStatus(HttpStatus.CREATED) //메소드의 실행 결과에 대한 HTTP 응답 코드를 HpptStatus.CREATED로 설정
public Response signUp(@Valid @RequestBody SignUpRequest req){ //@Valid : SignUpRequest 객체에 대한 유효성 검사, @RequestBody : 요청 바디에서 데이터를 추출하여, 매개변수로 전달된 객체에 매핑
signService.signUp(req); // 회원 가입 처리
return success();
}
@PostMapping("/api/sign-in")
@ResponseStatus(HttpStatus.OK)//메소드의 실행 결과에 대한 HTTP 응답 코드를 HpptStatus.OK로 설정
public Response signIn(@Valid @RequestBody SignInRequest req) {
return success(signService.signIn(req)); // 로그인 처리
}
}
위 코드의 상세 설명은 다음과 같습니다.
- @RestController : @Controller 과 Api를 처리하기 위한 @ResponseBody가 합쳐진 것입니다.
- @RequiredArgsConstructor : 클래스에서 final로 선언된 필드를 가진 생성자를 만들어 줍니다. (final 잊지 말 것)
- @PostMapping : Http POST 요청을 처리하는 핸들러 메소드를 지정
- @Valid : SignUpRequest, SignInRequest 객체에 대한 유효성을 검사
- @RequsetBody : 요청 바디에서 데이터를 추출하여, 매개변수로 전달된 객체에 매핑
Sign Controller 테스트
Sign Controller을 생성했으니 테스트를 진행해보겠습니다.
먼저 전체 코드 입니다.
@ExtendWith(MockitoExtension.class)
class SignControllerTest {
@InjectMocks SignController signController;
@Mock SignService signService;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void beforeEach(){
mockMvc = MockMvcBuilders.standaloneSetup(signController).build();
}
@Test
public void signUpTest() throws Exception{
//given
SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
// when, then
doNothing().when(signService).signUp(req); //signService.signUp 메소드의 리턴 값이 void이므로, doNothing()으로 Mocking
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated());
verify(signService).signUp(req); //signUp() 메소드가 호출되었는지 확인
}
@Test
void signInTest() throws Exception {
// given
SignInRequest req = new SignInRequest("email@email.com", "123456a!");
given(signService.signIn(req)).willReturn(new SignInResponse("access", "refresh"));
// when, then
mockMvc.perform(
post("/api/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.result.data.accessToken").value("access"))
.andExpect(jsonPath("$.result.data.refreshToken").value("refresh"));
verify(signService).signIn(req);
}
@Test
void ignoreNullValueInJsonResponseTest() throws Exception {
// given
SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
// when, then
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.result").doesNotExist());
}
}
- 테스트 함수 전의 설정들은 이전 테스트들과 동일하니 설명은 생략하겠습니다.
- contentType : JSON 형식의 데이터를 보내기 위해 설정
- content : 실제 데이터 설정
- andExpect : 예상되는 응담 상태코드가 맞는지 확인
회원가입, 로그인 DTO 수정
이번에는 이전에 만들었던 회원가입과 로그인에 대한 DTO 필드에 유혀성 검사를 추가하겠습니다.
SignUpRequest
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignUpRequest {
@Email(message = "이메일 형식을 맞춰주세요.")
@NotBlank(message = "이메일을 입력해주세요.")
private String email; // 1
@NotBlank(message = "비밀번호를 입력해주세요.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
message = "비밀번호는 최소 8자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.")
private String password; // 2
@NotBlank(message = "사용자 이름을 입력해주세요.")
@Size(min=2, message = "사용자 이름이 너무 짧습니다.")
@Pattern(regexp = "^[A-Za-z가-힣]+$", message = "사용자 이름은 한글 또는 알파벳만 입력해주세요.")
private String username; // 3
@NotBlank(message = "닉네임을 입력해주세요.")
@Size(min=2, message = "닉네임이 너무 짧습니다.")
@Pattern(regexp = "^[A-Za-z가-힣]+$", message = "닉네임은 한글 또는 알파벳만 입력해주세요.")
private String nickname; // 4
public static Member toEntity(SignUpRequest req, Role role, PasswordEncoder encoder){
return new Member(req.email, encoder.encode(req.password), req.username, req.nickname, List.of(role));
}
}
SignInRequest
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignInRequest {
@Email(message = "이메일 형식을 맞춰주세요.")
@NotBlank(message = "이메일을 입력해주세요.")
private String email; // 1
@NotBlank(message = "비밀번호를 입력해주세요.")
private String password; // 2
}
회원가입, 로그인 DTO 테스트
유효성 검사이기 때문에 테스트 양은 많지만 어렵지 않기 때문에 천천히 보시면 이해가 되리라 생각합니다.
SignUpRequest Test
class SignUpRequestValidationTest {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
void validateTest() {
// given
SignUpRequest req = createRequest();
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req); //유효성 검사를 실패한 속석을 저장
// then
assertThat(validate).isEmpty();
}
@Test
void invalidateByNotFormattedEmailTest() {
// given
String invalidValue = "email";
SignUpRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankEmailTest() {
// given
String invalidValue = " ";
SignUpRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyPasswordTest() {
// given
String invalidValue = null;
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankPasswordTest() {
// given
String invalidValue = " ";
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByShortPasswordTest() {
// given
String invalidValue = "12312a!";
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNoneAlphabetPasswordTest() {
// given
String invalidValue = "123!@#123";
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNoneNumberPasswordTest() {
// given
String invalidValue = "abc!@#abc";
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNoneSpecialCasePasswordTest() {
// given
String invalidValue = "abc123abc";
SignUpRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyUsernameTest() {
// given
String invalidValue = null;
SignUpRequest req = createRequestWithUsername(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankUsernameTest() {
// given
String invalidValue = " ";
SignUpRequest req = createRequestWithUsername(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByShortUsernameTest() {
// given
String invalidValue = "한";
SignUpRequest req = createRequestWithUsername(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNotAlphabetOrHangeulUsernameTest() {
// given
String invalidValue = "송2jae";
SignUpRequest req = createRequestWithUsername(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyNicknameTest() {
// given
String invalidValue = null;
SignUpRequest req = createRequestWithNickname(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankNicknameTest() {
// given
String invalidValue = " ";
SignUpRequest req = createRequestWithNickname(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByShortNicknameTest() {
// given
String invalidValue = "한";
SignUpRequest req = createRequestWithNickname(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByNotAlphabetOrHangeulNicknameTest() {
// given
String invalidValue = "송2jae";
SignUpRequest req = createRequestWithNickname(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyEmailTest() {
// given
String invalidValue = null;
SignUpRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignUpRequest>> validate = validator.validate(req);
// then
assertThat(validate).isNotEmpty();
assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
private SignUpRequest createRequest() {
return new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
}
private SignUpRequest createRequestWithEmail(String email) {
return new SignUpRequest(email, "123456a!", "username", "nickname");
}
private SignUpRequest createRequestWithPassword(String password) {
return new SignUpRequest("email@email.com", password, "username", "nickname");
}
private SignUpRequest createRequestWithUsername(String username) {
return new SignUpRequest("email@email.com", "123456a!", username, "nickname");
}
private SignUpRequest createRequestWithNickname(String nickname) {
return new SignUpRequest("email@email.com", "123456a!", "username", nickname);
}
}
SignInRequest Test
class SignInRequestValidationTest {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
void validateTest() {
// given
SignInRequest req = createRequest();
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isEmpty();
}
@Test
void invalidateByNotFormattedEmailTest() {
// given
String invalidValue = "email";
SignInRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isNotEmpty();
Assertions.assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyEmailTest() {
// given
String invalidValue = null;
SignInRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isNotEmpty();
Assertions.assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankEmailTest() {
// given
String invalidValue = " ";
SignInRequest req = createRequestWithEmail(invalidValue);
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isNotEmpty();
Assertions.assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByEmptyPasswordTest() {
// given
String invalidValue = null;
SignInRequest req = createRequestWithPassword(invalidValue);
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isNotEmpty();
Assertions.assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
@Test
void invalidateByBlankPasswordTest() {
// given
String invalidValue = " ";
SignInRequest req = createRequestWithPassword(" ");
// when
Set<ConstraintViolation<SignInRequest>> validate = validator.validate(req);
// then
Assertions.assertThat(validate).isNotEmpty();
Assertions.assertThat(validate.stream().map(v -> v.getInvalidValue()).collect(toSet())).contains(invalidValue);
}
private SignInRequest createRequest() { // 6
return new SignInRequest("email@email.com", "123456a!");
}
private SignInRequest createRequestWithEmail(String email) { // 7
return new SignInRequest(email, "123456a!");
}
private SignInRequest createRequestWithPassword(String password) {
return new SignInRequest("email@email.com", password);
}
}
궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.
'spring > 게시판 api' 카테고리의 다른 글
Spring boot 게시판 API 서버 제작 (8) - 로그인 - 조회, 삭제 로직 (0) | 2023.02.19 |
---|---|
Spring boot 게시판 API 서버 제작 (7) - 로그인 - Exception 처리 (0) | 2023.02.19 |
Spring boot 게시판 API 서버 제작 (5) - 로그인 - 서비스 로직 구현 (0) | 2023.02.18 |
Spring boot 게시판 API 서버 제작 (4) - 로그인 - 비밀번호 암호화 및 토큰 발급과 검증 (0) | 2023.02.18 |
Spring boot 게시판 API 서버 제작 (3) - 로그인 - create,update 시간 추가 & Repository 구현 (0) | 2023.02.17 |