[Computer Science] Server Side Validation

김상현·2023년 8월 13일
0

CS

목록 보기
2/10
post-thumbnail

Server Side Validation이란?

웹 서버에서 전송된 데이터로 연산을 수행하거나 데이터베이스에 값을 추가 및 수정할 때 해당 데이터의 값이 유효(타입, 길이, 패턴, ...)한지 서버 측에서 검사하는 것을 Server Side Validation이라고 한다.

데이터에 대한 유효성 검사는 대부분 클라이언트에서 수행한다. 사용자가 잘못 입력한 데이터에 대해 피드백을 서버를 거치지 않고 바로 전달할 수 있기 때문이다. 하지만 사용자가 악의적으로 웹브라우저의 스크립트를 조작하여 유효성 검사를 진행하지 않고 서버에 직접 데이터를 전달하게 될 경우 서버는 잘못된 연산을 수행하거나 유효하지 않은 데이터를 데이터베이스에 저장할 수도 있다. 오류 발생을 사전에 차단하기 위해서 클라이언트 측과 서버측 모두 입력된 데이터에 대한 유효성 검사를 하는 것이 이상적인 환경이라고 볼 수 있다.

Spring Server Side Validation 적용하기

사용자가 username, email, password 3개의 데이터를 서버에 보내 회원가입하는 기능에서 사용자로부터 전달 받은 3개의 데이터의 유효성이 올바른지 확인하는 유효성 검사를 Spring 프레임워크를 통해 테스트 한다.

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
}
  • validation 을 사용하기 위해 dependencies에 라이브러리를 추가한다.

User

@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를 필드로 추가

UserJoinReqDto

@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저라 문자열이어야 한다.

UserJoinResDto

@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;
    }
}

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}
  • 간편한 구현을 위해 Data Jpa를 사용하였다.
  • usernameemail 의 경우 유일성(unique)을 보장 받아야 하기 때문에 사용자로부터 전달 받은 usernameemail 값과 일치하는 데이터가 데이터베이스에 존재하는지 확인하기 위한 existsBy*** 메소드를 선언한다.

UserService

@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);
    }
}
  • usernameemail 값과 일치하는 데이터가 데이터베이스에 존재하는지 확인한다.
    • 만약 usernameemail 값과 일치하는 데이터가 데이터베이스에 존재할 경우 예외처리
  • UserJoinReqDto 데이터를 이용하여 User 객체를 생성하고 데이터베이스에 저장한다.

UserController

@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 에 메시지와 함께 저장된다.

UserControllerTest

@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));
    }

}

테스트 결과

profile
목적 있는 글쓰기

0개의 댓글