차근차근 Modern Spring Boot 3 기초 (7) DTO와 API

Merge SimpsonMerge Simpson
12 min read

페이지 요청과 AJAX 요청

우리는 두 요청 중 페이지 요청은 다루지 않습니다. 페이지 요청은 말 그대로 웹 페이지에 대한 요청이고, AJAX 요청은 쉽게 말하자면 이미 페이지를 받아서 띄운 상태에서, 추가적으로 데이터나 명령에 대하여 서버에 요청하는 것입니다.

페이지 요청을 다루지 않는 이유는, 최근 프론트엔드와 백엔드의 작업 영역, 배포하는 서버 등이 예전에 비해 뚜렷하게 구분되고 있기 때문입니다. 페이지 요청을 프론트엔드 쪽으로 하고, 데이터와 각종 명령을 백엔드 서버에 요청한다고 이해할 수 있습니다. 반드시 이렇게 나뉘는 것은 아닙니다.

페이지 요청

클라이언트에서 서버로 페이지를 요청합니다. 서버는 HTML 등 페이지의 정적 파일을 응답합니다.

이 역할은 예전에는 통합되어 있던 서버에서 많이 수행했지만, SPA(리액트 등) 진영이 성장하게 되면서 프론트엔드 서버에서 담당하는 편입니다. 서버라고 불렀지만, 정적 파일을 배포하는 스토리지에서 바로 응답하기도 합니다. 그 형태는 이번 공부에서 중요한 것은 아니고, 이번 스프링 부트 프로젝트는 페이지 요청을 다루지 않고 API 서버로 사용한다는 것이 중점입니다.

AJAX 요청

AJAX(Asynchronous JavaScript and XML)라는 개념을 쉽게 생각하면, 그냥 페이지를 띄워 놓은 상태에서 서버와 통신하는 것입니다. 페이지의 새로고침 없이 서버와 데이터를 주고 받거나, 명령을 주고 받는 기술입니다. 대표적으로 fetch API, XHR, axios 등 다양한 기술을 통해 구현되고 있습니다.

AJAX는 jQuery의 함수 이름이 아닙니다.

교육이나 실무에서 ajax라는 함수를 접한 개발자 중 일부는 ajax가 jQuery 함수 이름이라고 오해하곤 합니다. jQuery 라이브러리에 포함된 ajax 함수 때문인데, 사실 특정 라이브러리의 함수 이름이 아니라 자바스크립트에서 비동기 통신을 하기 위한 기술을 칭하는 표현입니다. "AJAX 대신 AXIOS나 fetch를 써라." 하는 식의 설명은 그러한 오해에서 비롯되었겠죠.

RestController

일반 컨트롤러 애노테이션(@Controller)을 사용하여 만든 클래스는, 페이지 요청과 Ajax 요청을 모두 처리할 수 있는 컨트롤러가 됩니다. 이러면 ajax 요청을 처리하기 위해서 몇 가지 작업이 추가되죠. 그런 작업을 매번 반복하지 않기 위해서, ajax 요청만 처리하는 컨트롤러용 애노테이션이 준비되었습니다.

@RestController 애노테이션을 클래스 위에 선언하면, 이 클래스는 이제 ajax 요청 처리용 컨트롤러 클래스가 됩니다.

@RestController
public class AuthApi {

}

이곳에서 use case(서비스)를 바로 사용할 수 있습니다. 빈으로 주입받도록 생성자를 만들어 줍니다.

@RestController
@RequiredArgsConstructor // final과 non-null 필드에 대한 생성자를 만듭니다.
public class AuthApi {
    private final SignUpUseCase signUpUseCase;

    // 곧 이곳에 메서드를 추가하여 API를 만들 것입니다.
}

요청과 응답용 DTO

DTO는 전달할 데이터 객체를 말합니다. 그 객체를 클래스로 표현할 수도 있으니, 작업하는 관점에서는 주고 받을 데이터의 양식(클래스)이라고 생각할 수도 있습니다. "우리가 만드는 API를 이용하려면, 이런 데이터를 주세요, 그러면 우리가 이런 데이터를 드리겠습니다."라고 할 때의 '데이터'들을 DTO라고 부를 수 있습니다.

DTO 모아 두기

패키지 내에 각각의 DTO용 클래스 파일을 여러 개 만들어도 사용하는 데에는 문제가 없습니다. 하지만 우리는 모던한 방향을 탐구하고 있고, 패키지 안에 너무 많은 파일이 있다면 이름순으로 정렬해도 원하는 클래스를 찾는 데에 눈과 손이 고생한다는 것을 예상할 수 있습니다. 그렇다면 조금 더 개선된 해결책을 찾는 것이 우리 탐구 방향에 맞을 것입니다.

그래서 하나의 클래스 파일을 만들고, 그 안에 내부 클래스로 필요한 DTO들을 분류하는 방안을 택하고 있습니다. 이때는 여러 request용 DTO와 response용 DTO를 어떤 식으로 정렬해 두어야 찾기 편한 배치인지 생각해 볼 수 있죠. 기능 단위로 1차 분류를 할 것인지 request/response로 묶어 1차 분류를 할 것인지 정할 수 있을 것입니다. 이때 제 본능과 경험상의 결론은 이렇습니다.

public final class AuthDto {
    private AuthDto() {}

    // [1] request DTO를 나열
    public record SignUpRequest(/* data */) {}

    public record SignInRequest(/* data */) {}

    // ...

    // [2] response DTO를 나열
    public record SignUpResponse(/* data */) {}

    public record SignInResponse(/* data */) {}
}

이와 같은 배치의 이유는 다음과 같습니다.

우리 뇌는 작업을 하다 보면 집중력이 감소합니다.

  • 막상 DTO를 찾으려고 할 때, 기능의 이름은 바로바로 떠오르지 않을 수도 있습니다.

  • 반면 request용 DTO를 찾으려고 한 것인지 response용 DTO를 찾으려고 한 것인지는 압니다.

  • 따라서 1차 분류가 request DTO와 response DTO로 되어 있는 것이 눈으로 찾기 더 편합니다.

기능 단위로 먼저 묶어서 배치하면 원하는 클래스를 찾기까지 생각보다 많은 스크롤을 요구할 겁니다.

  • 탐색 영역이 늘어난 만큼 사람의 눈으로 찾기는 더 불편할 겁니다.

  • 물론 대략적인 위치도 기억할 수 있고, 단어를 기억하면 금방 검색할 수 있습니다. 하지만 앞서 말한 대로 지속적인 작업에서 에너지를 절약할 수 있도록 정리해 두는 것이 낫습니다.

탐색 영역을 반으로 줄이려면, 상위에 request들을, 하위에 response들을 모아 놓으면 됩니다.

  • 네, 그뿐입니다. 이로써 우리 뇌가 주요 작업도 아닌 'DTO를 어디에 써 두었는지 찾기'를 위해 너무 많은 에너지를 쓰지 않아도 되죠.

물론 우리 체력이 남아 있을 때는 통상, 원하는 DTO를 찾기 위해 IDE에서 클래스 이름을 검색하는 것이 빠르고 편합니다. 그럼에도 어느 정도 기준으로 정리되어 있는 파일을 추구하겠다는 것이죠. 코드 스타일 관례와 마찬가지로, DTO 배치를 정리하는 데에 있어서 저와 비슷한 기준을 택하는 빅테크들이 있습니다.

SignUp DTO들 작성 (Record)

기본적으로 record는 클래스의 한 유형입니다. record는 불변 객체를 만들기 위한 클래스임과 동시에, 불필요한 코드 작성을 확연하게 줄여 주는 자바의 신기능입니다. Java 16 이상에서 사용할 수 있으며, 스프링부트 3 이상에서는 Java 호환성 버전이 17 이상이기 때문에 앞으로 신규 프로젝트에서는 대부분 사용이 가능할 것입니다.

불변객체란, 모든 필드가 final 필드인 객체라고 생각하면 됩니다. 당연한 선언이기 때문에, 생략하고 필드의 타입과 변수명만 작성하면 됩니다. 작성 시 알아야 할 내용은 다음과 같습니다.

  • 모든 필드를 채울 수 있는 All arguments constructor를 자동으로 생성합니다.

  • 클래스 선언과 동시에 마치 생성자를 작성하듯, 소괄호를 열고 그 안에 생성자의 파라미터겸 필드를 작성하면 됩니다(아래 SignUprequest). 아래 예시에서는 가독성을 위해 줄바꿈을 추가하였지만, 중괄호가 아닌 소괄호 안에 작성하였다는 것을 눈여겨 봐야 합니다.

public final class AuthDto {
    private AuthDto() {}

    @Builder
    public record SignUpRequest(
            String username,
            String password,
            String nickname
    ) {}

    public record SignInRequest(/* data */) {}

    // responses
    public record SignUpResponse(/* data */) {}

    public record SignInResponse(/* data */) {}
}

빌더 패턴 Builder Pattern

우리는 record를 사용할 때, 편의를 위해 롬복(lombok)의 @Builder라는 애노테이션을 붙여 줍니다. 여러 대안 중에서도 편의성에 중점을 둔 선택입니다. 이에 대해 일부 개발자가 반감을 보일 수도 있다는 예상을 하지만, 아직까지는 반감의 반응을 보지 않았습니다. (예상하는 반감의 반응은, 설계의 안정성을 보다 강력하게 추구하는 사람들로부터 나올 수 있다고 생각합니다. 아직 이 글의 독자님들이 빌더 사용의 장단점을 다루는 진도가 아니기 때문에, 지금은 편의를 위해 빌더를 사용한다고만 알아 두세요.)

빌더(builder) 패턴은 주로 생성자를 위하여 적용하는데, 생성자 파라미터 조합이 복잡하거나, 파라미터 타입이 많이 겹쳐서 원하는 파라미터 조합으로 생성자 오버로딩이 잘 안 되는 상황 등, 여러 필드에 대해 생성자를 관리하는 것이 어려울 때 매우 유용한 패턴입니다.

특히 위 예시처럼 생성자의 세 파라미터가 모두 String 타입이라면, 첫 번째 파라미터로 넣은 문자열이 username인지 password인지 nickname인지 타입으로는 헷갈릴 수 있습니다. 젊은 스타일을 띠는 언어들은 파라미터의 이름을 지정해서 대입하는 기능을 제공하지만, 자바는 언어 수준에서는 아직 그런 기능이 없습니다. 대신 롬복의 빌더를 통해 파라미터의 이름을 통한 대입을 지원함으로써, 문제를 해결할 수 있죠. 빌더의 사용은 다음과 같습니다.

  • 우선 클래스의 이름 뒤에 static 메서드인 .builder()를 통해 빌더를 호출할 수 있습니다.
    (@Builder 애노테이션이 자동으로 생성하는 함수 이름의 기본값입니다.)

  • 빌더가 호출되면, 그 뒤에 메서드 체이닝(메서드 뒤에 점을 찍어서 연결)을 통해 파라미터 이름으로 대입용 함수를 나열할 수 있습니다. (아래 사용례 확인)

  • 마지막에 빌더로부터 최종적으로 객체를 생성할 때는 .build() 함수를 사용합니다.

이처럼 빌더의 사용은 .builder()로 시작해서 .build()로 끝난다고 기억하면 쉽습니다.

SignUpRequest dto = SignUpRequest.builder()
        // 세 파라미터를 채우는 메서드는 사용 순서가 바뀌어도 됩니다.
        .username("abc123")
        .password("abc123!@")
        .nickname("고길동")
        .build();

유효성 관리(Validation)

프론트엔드에서의 유효성 체크는 보안보다 사용자의 편의성과 관련이 있습니다.
유효성 체크는 백엔드에서도 반드시 수행해야 합니다.

예를 들어 비밀번호는 영문, 숫자, 특수문자를 모두 포함하여 8자리 이상이어야 한다는 문구를 보신 적이 있을 겁니다. 이처럼 데이터의 입력 양식에 대해서 체크하는 것을 유효성 체크라고 합니다. 그렇다면 올바른 데이터 입력 양식을 위한 유효성 체크는 프론트엔드와 백엔드 중 어느 곳에서 수행해야 할까요?

유효성과 관련한 안내를 주로 사용자 화면에서 접했습니다. 그러다 보니 유효성 체크를 프론트엔드에서 관리한다고 생각하는 분들이 종종 계십니다. 물론 화면에서 안내되지 않는다면 사용자는 올바른 데이터 입력을 할 수 없기 때문에, 사용자의 편의성을 위해서 반드시 필요한 기능이기는 합니다. 하지만 보안을 위해서 작성한다는 취지는 논쟁의 쟁점이 될 수 있습니다. 클라이언트에서 보내는 요청 데이터는, 유효성 체크를 하는 프론트엔드의 로직을 모두 건너뛰고 개발자도구만 켜도 쉽게 변경해서 서버로 보낼 수 있기 때문입니다.

프론트엔드와 백엔드에서 유효성 체크의 실질적인 역할은 다음과 같습니다.

  • 프론트엔드의 유효성 체크: 사용자 편의성

  • 백엔드의 유효성 체크: 사용자 입력에 대한 보안 (각 데이터에 대한 정책 관리)

Java Bean Validation API와 Hibernate Validator

우리는 유효성(validation) 체크를 위해 다음 의존성 라이브러리를 추가했습니다. (build.gradle.kts)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

이 라이브러리가 추가되어 있다면 다음처럼 쉽게 기본적인 유효성 체크를 수행할 수 있습니다.


import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;

public final class AuthDto {
    private AuthDto() {}

    @Builder
    public record SignUpRequest(
            @NotBlank
            @Pattern(regexp = "^[a-z]+[a-z0-9]{3,30}$")
            String username,

            @NotBlank
            @Pattern(regexp = "(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,100}$")
            String password,

            @NotBlank
            @Pattern(regexp = "^[A-Za-z0-9ㄱ-ㅣ가-힣]{3,30}$")
            String nickname
    ) {}

    public record SignInRequest(/* data */) {}

    // responses
    public record SignUpResponse(/* data */) {}

    public record SignInResponse(/* data */) {}
}

주요 애노테이션에 대한 설명입니다. 필수 입력 항목(required item)을 체크하는 애노테이션이 없다면 다른 애노테이션은 null 등에 대해서 체크를 건너뛰기 때문에, 필수 입력 항목인 경우 관련 애노테이션을 함께 써야 합니다.

// 필수 입력 항목(required item)임을 표현하는 애노테이션
@NotNull // null이어선 안 됨. (required item)
@NotEmpty // null, 빈 배열 또는 빈 문자열("")이어선 안 됨.
@NotBlank // null, 빈 문자열, 공백 문자열(" ")이어선 안 됨.

// 정규 표현식 (필수 입력 항목인 경우 위 애노테이션들과 함께 사용)
@Pattern(regexp = "^[A-Za-z0-9ㄱ-ㅣ가-힣]{3,30}$")

// 정수 또는 big decimal의 boundary (inclusive)
@Min(0)
@Max(100)
@DecimalMin("0") // Long.MIN_VALUE보다 작은 범위도 지원
@DecimalMax(" ... ") // Long.MAX_VALUE보다 작은 범위도 지원
@Digits(integer = 10, fraction = 2) // 십진수에서 자릿수
// Digits의 주요 속성: 정수부 자릿수(integer), 소숫점 이하 자릿수(fraction)

// 길이
@Size(min = 0, max = 150) // 컬렉션, 맵, 배열, 문자열
@Length(min = 2, max = 30) // 문자열. (@Pattern에 통합 가능, @Size로 대체 가능)

// 날짜 (현재와 비교)
@Future
@FutureOrPresent // inclusive
@Past
@PastOrPresent // inclusive
// ex:
// @Future LocalDate date; // 내일부터
// @FutureOrPresent LocalDate date; // 오늘부터

// * 다른 조건에서 일시를 비교하기 위해서는 record의 compact 생성자 활용

// 이메일 (로컬@도메인 각 파트에서 로컬 최대 64글자, @ 1글자, 도메인 파트 최대 255글자)
@Email
// ex:
//  @Email @NotBlank @Size(max = 255) String email;

/* 표준에서는 이메일에: 로컬 + @ + 도메인을 합쳐 최대 320글자(RFC)
 *  서비스 운영상으로는 최대 255자 권장 (255를 넘으면 다른 이메일 사용 권장)
 *  점은 연속 두 개 이상 놓일 수 없음(.. 등)
 *  그 외 허용 특수 문자: ! # $ % & ' * + - / = ? ^ _ ` { | } ~
 *  도메인 파트도 각 label은 1~63글자 (점으로 구분된 파트)
 *  이 애노테이션 처리에서는 이메일 주소에 주석(소괄호)을 사용할 수 없음.
 *  퓨니코드(한글 등의 이메일 계정, 도메인) 허용됨.
 */

이 외에도 다양한 애노테이션이 있지만, 활용도가 높은 애노테이션은 위와 같습니다.

참고로 첫 설명이기 때문에 가독성을 위해 생략했지만, 위 애노테이션들은 message 속성과 함께 쓸 수 있습니다.

@NotBlank(message = "사용자 이름을 입력하세요.")
@Pattern(
        regexp = "^[a-z]+[a-z0-9]{3,30}$",
        message = "아이디는 알파벳 소문자와 숫자만 허용하며, 소문자로 시작해야 합니다. (3~30 글자)"
)

응답 DTO

응답 DTO에서는 각 데이터의 유효성을 다시 체크하지 않아도 됩니다. 정상적인 시스템을 구성하기 위해 서비스 수준에서 이미 자신이 담당하는 서버 측 자원을 올바르게 관리하도록 구성하고, DTO는 서버에서 제공하는 것을 신뢰하는 것이, 자원 관리의 일원화에 도움이 되는 구조입니다.

또는 서비스나 그 인근의 레이어에서 정책에 관한 기능을 제공해 주고, 이를 응답 DTO에서 확인하거나, 데이터 누락을 방지하기 위해서 롬복의 @NonNull 또는 Objects.requireNonNull() 등을 사용할 수 있습니다. 중요한 것은 유효성의 '결정 권한'을 응답 DTO에서 생성하지 않는 것입니다. 필수 입력 정도 체크는 할 수 있습니다.

다음은 유효성 체크를 하지 않는 응답 DTO를 작성한 코드입니다.


import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;

import java.time.Instant;

public final class AuthDto {
    private AuthDto() {}

    @Builder
    public record SignUpRequest(
            @NotBlank
            @Pattern(regexp = "^[a-z]+[a-z0-9]{3,30}$")
            String username,

            @NotBlank
            @Pattern(regexp = "(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,100}$")
            String password,

            @NotBlank
            @Pattern(regexp = "^[A-Za-z0-9ㄱ-ㅣ가-힣]{3,30}$")
            String nickname
    ) {}

    public record SignInRequest(/* data */) {}

    // responses
    public record SignUpResponse(
            @JsonInclude(Include.NON_NULL)
            String username,
            @JsonInclude(Include.NON_NULL)
            String nickname,
            @JsonInclude(Include.NON_NULL)
            Instant createdAt,
            @JsonInclude(Include.NON_NULL)
            AccountStatus status
    ) {}

    public record SignInResponse(/* data */) {}
}

스프링에서 JSON 객체를 만들거나 해석할 때 기본적으로 Jackson이라는 라이브러리를 사용합니다. 이런 기능을 가진 것들을 메시지 컨버터라고도 부릅니다. 설정을 바꾸지 않는 한 기본 메시지 컨버터는 Jackson입니다. 메시지 컨버터는 자바 객체를 JSON 문자열로, JSON 문자열을 자바 객체로 바꾸어 클라이언트와 주고 받습니다. 이 과정은 자동으로 수행됩니다.

JSON이란, 문자열로 데이터를 표현하는 방식 중 하나로 자바스크립트 객체의 작성 요령 중 한 가지를 정해서 양식화한 것입니다. 다음은 JSON 문자열의 예시입니다. 양쪽 끝 중괄호를 포함합니다.

{
  "username": "abc123",
  "nickname": "고길동",
  "status": "active"
}

자바스크립트의 null과 undefined

최근 프론트엔드 쪽에서는 타입스크립트를 통한 타입 제약 등 여러 이유로 JSON 반환에서 null 데이터 대신 그 필드를 완전히 JSON 문자열에 포함하지 않는 것을 선호하고 있습니다. Jackson에서는 이를 위한 기능으로 @JsonInclude(Include.NON_NULL) 애노테이션을 제공하고 있습니다. 프론트엔드에서 이 필드에 접근하면 null 대신 undefined를 반환받음으로써, 관리의 복잡성을 줄일 수 있습니다.

DTO를 사용한 컨트롤러 메서드

이제 사용자와 주고 받을 DTO와, 작업을 수행할 서비스를 모두 준비했으니, 컨트롤러에 API 메서드를 만들 수 있습니다.

다음 예시에서는 서비스 빈 사용에 필요한 데이터 변환을 위해 빌더를 사용한 부분이 조금 길어 보일 뿐, 아주 간단한 3 스텝으로 구성된 메서드입니다. 리팩토링을 하기 전까지는 이 코드를 먼저 이해해 봅시다.

import com.example.demo.auth.controller.dto.AuthDto.SignUpRequest;
import com.example.demo.auth.controller.dto.AuthDto.SignUpResponse;
import com.example.demo.auth.domain.Account;
import com.example.demo.auth.domain.AccountStatus;
import com.example.demo.auth.usecase.SignUpUseCase;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;

@RestController
@RequiredArgsConstructor
public class AuthApi {
    private final SignUpUseCase signUpUseCase;

    @PostMapping("/sign-up")
    @ResponseStatus(HttpStatus.CREATED)
    public SignUpResponse signUp(@RequestBody @Valid SignUpRequest body) {
        // [1] DTO를 Entity로 변환
        Account account = Account.builder()
                .username(body.username())
                .nickname(body.nickname())
                .password(body.password())
                .status(AccountStatus.ACTIVE)
                .createdAt(Instant.now())
                .build();

        // [2] 서비스에게 회원가입의 구체적인 동작을 위임.
        Account savedAccount = signUpUseCase.signUp(account);

        // [3] 결과 반환
        return SignUpResponse.builder()
                .username(savedAccount.getUsername())
                .nickname(savedAccount.getNickname())
                .createdAt(savedAccount.getCreatedAt())
                .status(savedAccount.getStatus())
                .build();
    }
}

PostMapping

우리 서버에 있는 이 함수로 요청을 보내려면 HTTP method와 요청할 경로가 필요합니다. 저 함수로 도착하기 위해서는 이런 정보가 필요합니다.

  • host: localhost (우리 단말기를 뜻하는 표현)

  • port: 8080 (스프링부트 애플리케이션을 실행할 때 기본값. 즉 내장 서버의 기본값)

  • path: /sign-up

  • HTTP 메서드: POST

스프링에서는 이것을 표현하기 위해서 RequestMapping이라는 것을 사용할 수도 있지만, 최근에는 좀 더 편하게 @GetMapping, @PostMapping 등을 사용할 수 있습니다. HTTP 메서드에 따라서, GET, POST, PUT, PATCH, DELETE 등을 위한 애노테이션이 있습니다.

호스트와 포트를 포함하면, 위 함수에 요청을 보내기 위해서는 다음 경로와 메서드로 보낼 수 있습니다.

  • URL: http://localhost:8080/sign-up

  • HTTP 메서드: POST

응답 상태코드(HTTP 상태 코드 중 응답에 쓰이는 것들)

  • 200번대: 정상 응답 (200 OK, 201 Created, 204 No Content 등)

  • 400번대: 클라이언트 측에서 핸들링할 수 있는 오류 응답

  • 500번대: 서버 사이드의 오류에 대한 응답

HTTP 요청에 대한 응답은 크게 위와 같이 분류합니다. 스프링에서 응답 상태코드를 작성할 때, 다음과 같은 수단을 사용하는 편입니다.

  • ResponseEntity 객체를 통해 상태 코드를 직접 결정합니다. (아직 다루지 않습니다.)

  • @ResponseStatus(HttpStatus.상태코드_선택) 애노테이션을 통해 미리 지정합니다.

저는 이중에서 정상 응답 코드는 @ResponseStatus 애노테이션을 통해 지정하는 것을 선호합니다.

API 쏴 보기

Postman을 열고 Collection 생성(마음대로 정렬 안 됨) > Folder 생성(정렬 가능) > Request 생성 후, 다음 내용대로 작성합니다. (컬렉션, 폴더, 요청 이름은 마음대로 작성해도 됩니다.)

  • HTTP Method는 POST입니다.

  • URL은 http://localhost:8080/sign-up으로 합니다.

  • 요청 body는 body > raw > json을 선택하여 JSON 문자열로 된 바디를 작성합니다.

      {
          "username": "abc123",
          "password": "abc123!@",
          "nickname": "고길동"
      }
    
    • 사용자 이름(아이디)은 알파벳 소문자와 숫자를 조합하여 3~30글자이며, 소문자로 시작해야 합니다.

    • 비밀번호는 영문, 숫자, 특수문자를 모두 사용하여 8~100글자로 입력하세요.

    • 닉네임은 영문, 숫자, 한글로 3~30글자입니다.

응답 확인

Send 버튼을 누르고 하단에 응답을 확인할 수 있습니다.

Enum 열거 상수를 소문자로 응답하기

이 부분은 의무적인 것은 아닙니다. 프론트엔드와 논의해서 대문자로 된 열거 상수를 사용하자고 하여도 되고, 소문자로 통일해도 됩니다. 하지만 자바에서는 열거 상수 네이밍에 대문자를 그대로 사용하겠죠.

import com.fasterxml.jackson.annotation.JsonProperty;

public enum AccountStatus {
    @JsonProperty("pending")
    PENDING,
    @JsonProperty("active")
    ACTIVE,
    @JsonProperty("protected")
    PROTECTED,
    @JsonProperty("suspended")
    SUSPENDED,
    @JsonProperty("slept")
    SLEPT,
    @JsonProperty("removed")
    REMOVED
}

이제 API를 다시 이용하면 소문자로 된 status 필드 값을 확인할 수 있습니다.

enum이 아니더라도 다음 예시처럼 활용할 수 있습니다. 예를 들어 외부와 데이터를 주고 받는 기술에서 특정 필드의 표준 네이밍이 snake_case로 되어 있다면, 자바에서 camelCase 등 관례를 유지하면서 JSON 문자열에서만 필드 이름을 변경할 수 있습니다. (앞서 enum은 value에서 네이밍 변경, 이번에 사용하는 예시는 key에서 네이밍 변경으로 볼 수 있습니다.)

// 예시
public record ExampleResponse(
        @JsonProperty("access_token")
        String accessToken
) {}

이러면 JSON 문자열은 다음과 같습니다. (Jackson 사용 시)

{"access_token": "..."}

< Prev

서비스 인터페이스의 세분화와 빈 불러오기

Next >

DTO와 Entity의 Mapping

0
Subscribe to my newsletter

Read articles from Merge Simpson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Merge Simpson
Merge Simpson

Hello, I am Korean. Welcome, visitor. You are very cool. 안녕하세요, 저는 한국어입니다. 방문자여 환영한다. 당신은 매우 시원해.