웹 서버에서 전송된 데이터로 연산을 수행하거나 데이터베이스에 값을 추가 및 수정할 때 해당 데이터의 값이 유효(타입, 길이, 패턴, ...)한지 서버 측에서 검사하는 것을 Server Side Validation
이라고 한다.
데이터에 대한 유효성 검사는 대부분 클라이언트에서 수행한다. 사용자가 잘못 입력한 데이터에 대해 피드백을 서버를 거치지 않고 바로 전달할 수 있기 때문이다. 하지만 사용자가 악의적으로 웹브라우저의 스크립트를 조작하여 유효성 검사를 진행하지 않고 서버에 직접 데이터를 전달하게 될 경우 서버는 잘못된 연산을 수행하거나 유효하지 않은 데이터를 데이터베이스에 저장할 수도 있다. 오류 발생을 사전에 차단하기 위해서 클라이언트 측과 서버측 모두 입력된 데이터에 대한 유효성 검사를 하는 것이 이상적인 환경이라고 볼 수 있다.
사용자가 username
, email
, password
3개의 데이터를 서버에 보내 회원가입하는 기능에서 사용자로부터 전달 받은 3개의 데이터의 유효성이 올바른지 확인하는 유효성 검사를 Spring 프레임워크를 통해 테스트 한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
validation
을 사용하기 위해 dependencies에 라이브러리를 추가한다.@Entity(name = "users")
@Getter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(unique = true)
private String username;
@Column(unique = true, updatable = false)
private String email;
private String password;
@Builder
public User(Long id, String username, String email, String password) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
}
}
id
와 회원가입 기능에 필요한 username
, email
, password
를 필드로 추가@Getter
@Setter
public static class UserJoinReqDto {
@NotEmpty(message = "Name cannot be empty")
@Pattern(regexp = "^[a-zA-Z가-힣]{2,20}$")
private String username;
@Email
@NotEmpty(message = "Email cannot be empty")
private String email;
@NotEmpty(message = "Password cannot be empty")
@Pattern(regexp = "^[a-zA-Z0-9]{8,20}$")
private String password;
@Builder
public UserJoinReqDto(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
public UserJoinReqDto() {
}
}
username
의 경우 필수적으로 존재해야 하고, 알파벳과 한글로 구성된 2~20자리 문자열이어야 한다.email
의 경우 필수적으로 존재해야 하고, 이메일 형식에 맞는 문자열이어야 한다.password
의 경우 필수적으로 존재해야 하고, 알파벳과 숫자로 구성된 8~10저라 문자열이어야 한다.@Data
public static class UserJoinResDto {
private Long id;
private String name;
private String email;
public static UserJoinResDto create(User user) {
return UserJoinResDto.builder()
.id(user.getId())
.name(user.getUsername())
.email(user.getEmail())
.build();
}
@Builder
public UserJoinResDto(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
username
과 email
의 경우 유일성(unique
)을 보장 받아야 하기 때문에 사용자로부터 전달 받은 username
과 email
값과 일치하는 데이터가 데이터베이스에 존재하는지 확인하기 위한 existsBy***
메소드를 선언한다.@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public UserJoinResDto save(UserJoinReqDto userJoinReqDto) {
if (userRepository.existsByUsername(userJoinReqDto.getUsername())) {
throw new IllegalArgumentException("Username already taken!");
}
if (userRepository.existsByEmail(userJoinReqDto.getEmail())) {
throw new IllegalArgumentException("Email already taken!");
}
User createUser = User.builder()
.username(userJoinReqDto.getUsername())
.email(userJoinReqDto.getEmail())
.password(userJoinReqDto.getPassword())
.build();
User savedUser = userRepository.save(createUser);
return UserJoinResDto.create(savedUser);
}
}
username
과 email
값과 일치하는 데이터가 데이터베이스에 존재하는지 확인한다.username
과 email
값과 일치하는 데이터가 데이터베이스에 존재할 경우 예외처리UserJoinReqDto
데이터를 이용하여 User
객체를 생성하고 데이터베이스에 저장한다.@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity save(@Valid @RequestBody UserJoinReqDto userJoinReqDto,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(fieldError -> errors.put(fieldError.getField(), fieldError.getDefaultMessage()));
return new ResponseEntity(errors, BAD_REQUEST);
}
UserJoinResDto userJoinResDto = userService.save(userJoinReqDto);
return new ResponseEntity<>(userJoinResDto, CREATED);
}
}
@RequestBody
앞에 @Valid
어노테이션을 통해 UserJoinReqDto
데이터에 설정한 유효성과 맞지 않는 데이터가 매핑된다면 BindingResult
에 메시지와 함께 저장된다.@AutoConfigureMockMvc
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class UserControllerTest {
@Autowired private MockMvc mvc;
@Autowired private ObjectMapper mapper;
@Test
void joinSuccessTest() throws Exception {
UserJoinReqDto userJoinReqDto = UserJoinReqDto.builder().
username("username").email("user@email.com").password("abcd1234").build();
String content = mapper.writeValueAsString(userJoinReqDto);
ResultActions resultActions = postResultActions("/api/user", content);
resultActions.andExpect(status().isCreated());
}
@Test
void joinFailByNameTest() throws Exception {
UserJoinReqDto userJoinReqDto = UserJoinReqDto.builder().
username("1").email("user@email.com").password("abcd1234").build();
String content = mapper.writeValueAsString(userJoinReqDto);
ResultActions resultActions = postResultActions("/api/user", content);
resultActions.andExpect(status().isBadRequest());
}
@Test
void joinFailByEmailTest() throws Exception {
UserJoinReqDto userJoinReqDto = UserJoinReqDto.builder().
username("username").email("email.com").password("abcd1234").build();
String content = mapper.writeValueAsString(userJoinReqDto);
ResultActions resultActions = postResultActions("/api/user", content);
resultActions.andExpect(status().isBadRequest());
}
@Test
void joinFailByPasswordTest() throws Exception {
UserJoinReqDto userJoinReqDto = UserJoinReqDto.builder().
username("username").email("user@email.com").password("a").build();
String content = mapper.writeValueAsString(userJoinReqDto);
ResultActions resultActions = postResultActions("/api/user", content);
resultActions.andExpect(status().isBadRequest());
}
ResultActions postResultActions(String url, String content) throws Exception {
return mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(content));
}
}