[Spring Boot] 왜 굳이 DTO를 써야 할까? 유효성 검증까지 한번에 이해하기 (feat. 회원가입)

Rose·2025년 6월 18일

Spring 기초 지식

목록 보기
1/1
post-thumbnail

Spring을 공부하다 보면 한 번쯤 듣게 되는 개념, DTO (Data Transfer Object).

처음에는 "뭔가 계층 간 데이터를 전달하는 용도인가보다~" 하고 지나쳤지만, 실제로 회원가입 기능을 만들면서 DTO가 왜 필요한지, 어떻게 써야 하는지 체감하게 되었습니다.

이 글에서는 직접 회원가입 기능을 구현하면서

  • DTO를 왜 따로 만드는것이 좋은지
  • 유효성 검증 과정에서 어떤 문제가 발생했는지
  • 그리고 어떻게 해결했는지

제가 겪은 과정을 기반으로 정리해보려고 합니다.


DTO란?

DTO (Data Transfer Object)는 계층 간 데이터 전달을 위해 사용하는 순수 데이터 전송 객체입니다. 웹 애플리케이션에서는 사용자의 입력값을 안전하게 받기 위한 전용 폼 객체로 자주 사용됩니다.

🤔 왜 굳이 DTO를 써야 할까?

처음엔 이렇게 생각했습니다.

"User 엔티티를 그냥 써도 되지 않나? 굳이 SignupFormDto 같은 걸 왜 만들어야 하지?"


회원가입 기능을 만들며 겪은 실제 흐름

처음엔 이렇게 User 엔티티를 그냥 컨트롤러에 받아서 처리했습니다.

@PostMapping("/signup")
public String signup(User user) {
    userRepository.save(user);
    return "redirect:/login";
}

간단해 보이지만 몇 가지 문제가 있었습니다.

User 엔티티를 바로 받으면 생기는 문제

문제설명
비즈니스 목적과 불일치회원가입 시 필요한 필드만 받으면 되는데, 불필요한 필드까지 열려있음
보안상 위험User 내부에 있는 민감한 필드까지 클라이언트에서 보낼 수 있음
유지보수 어려움나중에 User 엔티티가 바뀌면 폼 로직이 다 깨짐

그래서 등장한 DTO

회원가입에 필요한 필드만 딱 담은 전용 객체 SignupFormDto를 만들었습니다.

@Data
public class SignupFormDto {

    @NotBlank(message = "아이디는 필수입니다.")
    private String userId;

    @NotBlank(message = "비밀번호는 필수입니다.")
    private String password;

    @NotBlank(message = "이름은 필수입니다.")
    private String username;

    @Email(message = "이메일 형식이 올바르지 않습니다.")
    @NotBlank(message = "이메일 입력은 필수입니다.")
    private String email;

    @NotBlank(message = "소속 선택은 필수입니다.")
    private String affiliation; //학생대, 교육대

    @NotBlank(message = "중대 선택은 필수입니다.")
    private String unit; //1~3중대
}

오직 회원가입 시 사용자가 입력한 정보만 받도록 설계했습니다.

그런데 검증이 안된다?

@NotBlank, @Email을 다 붙였는데 값 안넣고 제출해도 그대로 통과되는 문제가 발생했습니다.

그 이유는 바로 @Valid와 BindingResult의 위치와 사용 여부 때문이었습니다.

유효성 검증이 동작하려면

컨트롤러는 다음처럼 구성되어야 합니다.

@PostMapping("/signup")
    public String processSignup(@Valid @ModelAttribute("signupForm") SignupFormDto form, BindingResult bindingResult, Model model) {

        if (bindingResult.hasErrors()) {
            return "signup"; // 유효성 에러 처리
        }

        try {
            signupService.registerUser(form);
            return "redirect:/login?signupSuccess"; //회원가입 성공 시 로그인 페이지로
        } catch (IllegalArgumentException e) {
            model.addAttribute("error", e.getMessage());
            return "signup";
        }
    }
요소역할
@ValidDTO에 붙은 @NotBlank 등의 어노테이션을 실제로 작동시킴
BindingResult유효성 검사 결과를 잡아주는 바구니 역할

❗️ 순서 주의: 반드시 @Valid → BindingResult 순서로 선언

처음엔 try-catch문이 있는데 왜 유효성 에러처리가 안되지? 라고 생각했는데 @Valid 유효성 검사는 컨트롤러 메서드 호출 전에 실행되기 때문이었습니다.

BindingResult가 없으면?
: 검증 실패 시 Spring이 MethodArgumentNotValidException을 던지고 예외로 끝냅니다 → try-catch 안으로 못 들어옵니다.

폼 페이지 (Thymeleaf)

<form th:action="@{/signup}" th:object="${signupForm}" method="post">
  <input type="text" th:field="*{userId}" />
  <small th:if="${#fields.hasErrors('userId')}" th:errors="*{userId}"></small>

  <input type="text" th:field="*{email}" />
  <small th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></small>

  <select th:field="*{affiliation}">
    <option value="">📌 선택하세요</option>
    <option value="학생대">학생대</option>
    <option value="교육대">교육대</option>
  </select>
  <small th:if="${#fields.hasErrors('affiliation')}" th:errors="*{affiliation}"></small>
</form>

자 그럼 이렇게 DTO를 쓸 경우 어떤 점이 좋아질까요?

DTO 사용 시 장점

장점설명
유효성 검증 최적화@NotBlank, @Email, @Size 등을 자유롭게 붙여 검증 가능
보안성 강화민감한 필드를 사용자에게 노출하지 않음
필요한 필드만 사용회원가입, 로그인, 비밀번호 변경 등 용도에 맞는 DTO 설계 가능
구조적 유연성DTO는 컨트롤러~뷰 전용 객체라, Entity가 바뀌어도 독립적으로 유지 가능

정리

  • 회원가입 같은 폼에서는 User 엔티티를 직접 사용하지 않는 것이 좋습니다.
  • User엔티티는 DB 저장용 객체이고, 사용자의 입력값은 DTO로 분리하는 것이 좋습니다.
  • 유효성 검증은 DTO에 어노테이션으로 붙이고 컨트롤러에서는 @Valid, BindingResult로 처리해야 합니다.
  • Thymeleaf에서는 th:field, th:errors를 제대로 써야 오류 메시지 표시가 가능합니다.

결론

DTO는 단순한 데이터 전달용 객체가 아닙니다. 사용자 입력을 안전하게 받고, 검증하고, 분리해서 처리하기 위한 아키텍처적 보호막이자, 유지보수성과 보안성을 높이는 필수 도구입니다.

profile
개발자를 꿈꾸며, 하루하루 쌓아가는 로제의 지식 아카이브입니다.

0개의 댓글