회원가입 기능
userName, password, emailAddress를 입력받고 userName이 DB에 존재하는지 중복 Check한 후, 이상이 없다면 저장
Talend 예상결과
👉 resultCode에 어떤 이유인지 출력하고, result는 추후 기능 추가 예정
UserController
🔴 @RequiredArgsConstructor
: 생성자가 필요한 부분을 자동으로 생성
(ex. UserService의 생성자를 자동으로 생성해줌)
🔸 join
UserJoinRequest
: userName, password, emailAddress)UserDto
: 변수는 User와 동일하나 User를 직접적으로 사용하지 않기 위해 사용Response.success
: 제네릭이 사용됐기 때문에 입력받은 매개변수 타입에 맞춰 리턴 ➡ UserJoinResponse를 입력받아 UserJoinResponse 리턴)UserJoinResponse
: userName, emailAddress)import com.hospitalreview.service.UserService;
import com.hospitalreview.domain.Response;
import com.hospitalreview.domain.dto.UserDto;
import com.hospitalreview.domain.dto.UserJoinRequest;
import com.hospitalreview.domain.dto.UserJoinResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/join")
public Response<UserJoinResponse> join(@RequestBody UserJoinRequest userJoinRequest) {
UserDto userDto = userService.join(userJoinRequest);
return Response.success(new UserJoinResponse(userDto.getUserName(), userDto.getEmailAddress()));
}
}
Response
🔹 error, success : 모두 static으로 처리됐기 때문에 어디서든 사용 가능
resultCode
: 어떤 에러인지 표시, null
: 추후 기능 추가 예정)SUCCESS
: 성공 메세지, result
: 제네릭을 사용했지만 주로 UserJoinResponse(userName, emailAddress)형식으로 리턴되며 어떤 데이터가 저장되는지 알 수 있음) import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class Response<T> {
private String resultCode;
private T result;
public static Response<Void> error(String resultCode) {
return new Response<>(resultCode, null);
}
public static <T> Response<T> success(T result) {
return new Response<>("SUCCESS", result);
}
}
UserDto
🔹 User을 직접적으로 다루지 않기 위해 사용
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class UserDto {
private Long id;
private String userName;
private String password;
private String emailAddress;
}
UserJoinRequest
🔹 회원가입할 때 입력받을 정보(userName, password, emailAddress)
import com.hospitalreview.domain.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class UserJoinRequest {
private String userName;
private String password;
private String emailAddress;
public User toEntity(){
return User.builder()
.userName(this.userName)
.password(this.password)
.emailAddress(this.emailAddress)
.build();
}
}
UserJoinResponse
🔹 회원가입이 성공했을 때 저장된 정보를 띄우기 위한 클래스 (단, 보안으로 인해 password는 생략)
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@Getter
public class UserJoinResponse {
private String userName;
private String emailAddress;
}
User
🔹 user 테이블과 연결할 클래스
(@Column(unique = true)
: 해당 열의 unique 옵션 적용)
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Getter
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String userName;
private String password;
private String emailAddress;
}
ErrorCode
🔹 발생하는 에러코드를 미리 담아놓는 클래스
🔴 enum
: 미리 지정 해놓고 그 값 말고 다른 값들을 넣지 못하게 하여 예측한 범위 내에서 프로그램이 작동하도록 하기 위한 기능
(ex. 아래의 경우. DUPLICATED_USER_NAME
를 미리 지정)
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@AllArgsConstructor
@Getter
public enum ErrorCode {
DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "User name is duplicated.");
private HttpStatus status;
private String message;
}
ExceptionManager
🔹 예외처리를 관리하는 클래스
🔴 @RestControllerAdvice
: 전역적으로 예외를 처리할 수 있는 어노테이션 (응답을 JSON으로 해줌)
import com.hospitalreview.domain.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionManager {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> runtimeExceptionHandler(RuntimeException e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Response.error(e.getMessage()));
}
@ExceptionHandler(HospitalReviewException.class)
public ResponseEntity<?> hospitalReviewExceptionHandler(HospitalReviewException e){
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(Response.error(e.getErrorCode().name()));
}
}
HospitalReviewException
🔹 RuntimeException을 상속받은 HospitalReviewException 클래스
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class HospitalReviewException extends RuntimeException{
private ErrorCode errorCode;
private String message;
@Override
public String toString() {
if(message == null) return errorCode.getMessage();
return String.format("%s. %s", errorCode.getMessage(), message);
}
}
UserRepository
🔹 DB의 user 테이블과 연결
(findByUserName
: userName의 중복체크를 위한 메소드 생성)
import com.hospitalreview.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUserName(String userName);
}
UserService
import com.hospitalreview.domain.User;
import com.hospitalreview.domain.dto.UserDto;
import com.hospitalreview.domain.dto.UserJoinRequest;
import com.hospitalreview.exception.ErrorCode;
import com.hospitalreview.exception.HospitalReviewException;
import com.hospitalreview.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public UserDto join(UserJoinRequest request){
// 비즈니스 로직 - 회원 가입
// 회원 userName(id) 중복 Check
// 중복이면 회원가입 X -> Exception(예외) 발생
userRepository.findByUserName(request.getUserName())
.ifPresent(user -> {
throw new HospitalReviewException(ErrorCode.DUPLICATED_USER_NAME, String.format("Username:%s", request.getUserName()));
});
// 중복 Check 통화하면 회원가입 -> .save()
User savedUser = userRepository.save(request.toEntity());
return UserDto.builder()
.id(savedUser.getId())
.userName(savedUser.getUserName())
.emailAddress(savedUser.getEmailAddress())
.build();
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hospitalreview.domain.dto.UserDto;
import com.hospitalreview.domain.dto.UserJoinRequest;
import com.hospitalreview.exception.ErrorCode;
import com.hospitalreview.exception.HospitalReviewException;
import com.hospitalreview.service.UserService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
UserService userService;
@Test
@DisplayName("회원가입 성공")
void join_success() throws Exception {
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("naver")
.password("naver123")
.emailAddress("email.@naver.com")
.build();
when(userService.join(any())).thenReturn(mock(UserDto.class));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("회원가입 실패")
void join_fail() throws Exception {
UserJoinRequest userJoinRequest = UserJoinRequest.builder()
.userName("naver")
.password("naver123")
.emailAddress("email.@naver.com")
.build();
when(userService.join(any())).thenThrow(new HospitalReviewException(ErrorCode.DUPLICATED_USER_NAME, ""));
mockMvc.perform(post("/api/v1/users/join")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(userJoinRequest)))
.andDo(print())
.andExpect(status().isConflict());
}
}
🔸 join_success()
: userJoinRequest로 요청이 왔을 때, mockMvc를 활용하여 일치하는지 테스트
🔸 join_fail()
: 실패했을 때 예외처리가 잘 되는지 확인해야함 ➡ thenThrow로 처리
➡ 🔴 마지막 줄에 isConflict
로 한 이유 : ErrorCode의 DUPLICATED_USER_NAME에 conflict로 설정했기 때문