[사이드 프로젝트] 디지털 굿즈 플랫폼 개발기 #5 - Controller 구현과 입력값 유효성 검증

지난 포스팅까지 우리는 데이터를 저장하는 Repository비즈니스 로직을 수행하는 Service를 구현했다. 하지만 이 코드들은 아직 내 컴퓨터 안에서만 동작할 뿐, 외부 세상(프론트엔드, 앱)과는 단절되어 있다.

이번에는 클라이언트의 요청을 받아 서비스 계층으로 연결해 주는 컨트롤러(Controller) 를 구현하고, 들어오는 데이터를 안전하게 검사하는 유효성 검증(Validation) 과정을 다룬다.


1. 컨트롤러의 책임: "선은 넘지 말자"

초보 개발자가 흔히 하는 실수 중 하나는 컨트롤러에 비즈니스 로직을 넣는 것이다. 컨트롤러는 "교통 정리"만 해야 한다.

  • 해야 할 일 (O): HTTP 요청받기, 입력값 검증(@Valid), 서비스 호출, HTTP 상태 코드(200, 201, 400...) 결정.
  • 하지 말아야 할 일 (X): DB 조회, 데이터 가공, 복잡한 계산 (이건 Service의 몫).

우리는 철저하게 DTO(Data Transfer Object) 를 사용하여 요청을 받고, 서비스에게 넘겨주는 구조를 택했다.


2. DTO와 유효성 검증 (@Valid)

회원가입 요청 시 이메일이 비어있거나, 비밀번호가 너무 짧다면 아예 서비스 로직을 태울 필요도 없다. 입구 컷(?)을 시키는 것이 효율적이다.

이를 위해 spring-boot-starter-validation 의존성을 활용하여 DTO에 검증 로직을 태그 해 두었다.

// MemberSignupRequest.java
public record MemberSignupRequest(
        @NotBlank(message = "이메일은 필수입니다.")
        @Email(message = "올바른 이메일 형식이 아닙니다.")
        String email,

        @NotBlank(message = "비밀번호는 필수입니다.")
        @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
        String password,
        
        @NotBlank String nickname
) {}

이제 컨트롤러는 이 규칙을 확인하기만 하면 된다.


3. MemberController 구현

@RestController를 사용하여 JSON 데이터를 주고받는 REST API를 구현했다.

@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    /**
     * 회원가입 API
     * [POST] /api/v1/members/signup
     */
    @PostMapping("/signup")
    // @Valid: RequestBody로 들어온 DTO의 유효성을 검사한다. 실패 시 예외 발생.
    public ResponseEntity<Long> signup(@Valid @RequestBody MemberSignupRequest request) {
        
        // 1. 서비스 로직 호출 (컨트롤러는 거들 뿐)
        Long memberId = memberService.signup(request);
        
        // 2. 결과에 맞는 HTTP 상태 코드 반환 (생성은 201 Created가 정석)
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(memberId);
    }
}

핵심 포인트

  1. @Valid: 이 어노테이션이 있어야 DTO에 붙여둔 @NotBlank, @Email 등이 실제로 동작한다. 만약 검증에 실패하면 스프링은 MethodArgumentNotValidException을 발생시킨다.
  2. ResponseEntity: 단순히 객체만 리턴하면 무조건 200 OK가 나가지만, 회원가입은 "리소스 생성"이므로 201 Created를 명시적으로 보내주는 것이 RESTful한 설계다.

4. Postman 테스트

서버를 실행하고 실제 요청을 보내보았다.

  • 요청 (성공 케이스):
    • URL: http://localhost:8080/api/v1/members/signup
{
    "email": "test@test.com",
    "password": "password1234",
    "nickname": "tester"
}
  • 응답:
    • Status: 201 Created
    • Body: 1 (생성된 회원 ID)

API가 정상적으로 뚫렸다! 이제 프론트엔드와 통신할 준비가 끝난 것이다.


만약 이메일 형식을 틀리게 보내면 어떻게 될까? @Valid가 작동하여 에러가 발생한다

다음 포스팅에서는 GlobalExceptionHandler 를 도입하여, 시스템에서 발생하는 모든 에러를 가로채고 JSON으로 응답하는 방법을 다뤄보겠다.