지난 포스팅까지 우리는 데이터를 저장하는 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);
}
}
핵심 포인트
- @Valid: 이 어노테이션이 있어야 DTO에 붙여둔 @NotBlank, @Email 등이 실제로 동작한다. 만약 검증에 실패하면 스프링은 MethodArgumentNotValidException을 발생시킨다.
- 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으로 응답하는 방법을 다뤄보겠다.
