[자바 Spring] DTO에 대해 배워보자
DTO(Data Transfer Object)란 계층간 데이터 교환을 위해 사용하는 객체(Java Beans)입니다. DTO 중 T인 Transfer라는 의미에서 알 수 있듯이 데이터를 전송하기 위한 용도의 객체 정도로 생각할 수 있습니다.
일반적으로 웹에서 데이터 전송은 클라이언트에서 서버 쪽으로 전송하는 요청 데이터, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터의 형식으로 이루어집니다.
즉, "DTO는 서버와 클라이언트의 데이터 전송에 사용하는 객체이다"라고 이해해 주시면 좋을 것 같습니다.
# DTO가 필요한 이유
- 코드의 간결성
- 유효성 검증
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
위 Controller 클래스 코드에서 다른 부분은 보실 필요 없이 postMember 메소드의 parameter인 세개의 @RequestParam() 부분만 집중해 보시면 됩니다.
**@RequestParam의 역할
예를들어, 여러분이 특정 웹 사이트에 회원가입을 핟다고 가정하겠습니다.
회원가입 화면의 정보기입 란에
- email - hrd@naver.com
- name - 홍길동
- phone - 010-1234-5678
다음과 같이 작성하고 회원 가입 완료 버튼을 누르면 해당 웹 사이트는 자신의 서버에 여러분들의 정보를 전송합니다.
이때 전송하는 각각의 email, name, phone의 값들을 받는 것이 @RequestParam 입니다. 위에서 @RequestParam() 을 통해 전달받은 email, name phone 변수 안에는 여러분이 회원가입 란에서 입력하였던 정보들이 그대로 저장되어 있을 것입니다.
@RequestParam()은 위 역할 설명처럼 key-value 형식으로 데이터를 받아들이지만 만약 저장해야할 데이터가 3개가 아닌 100개 또는 무수히 많다면 어떨까요? 코드가 굉장히 복잡해 질것입니다.
DTO(Data Transfer Object)를 사용하는 첫번째 이유는 '코드의 단순화' 때문입니다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(MemberDto memberDto) {
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
}
...
...
}
처음 나왔던 코드에 DTO를 적용하면 위 같은 코드로 변환되게 됩니다. 한눈에 봐도 훨씬 단순화 된 것을 확인할 수 있습니다. 위에 나오는 MemberDto는 클래스입니다. 즉, 원래 @RequestParam() 하나당 한개의 데이터를 받아 들였던 이전 코드와는 다르게 DTO는 데이터가 서버에 들어오는 동시에 MemberDto 클래스로 데이터를 전달하여 할당하는 기능을 수행합니다.
MemberDto 클래스는 이런 형태입니다.
public class MemberDto {
@Email
private String email;
private String name;
private String phone;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
이전에 우리가 회원가입 란에 작성하였던 각각의 email, name, phone의 값들도 MemberDto 내부에 있는 각각의 변수들에 저장되게 됩니다.
그런데 위 코드에서 처음보는 에노테이션을 확인할 수 있으실 겁니다. 바로 email 변수 바로 위에 있는 @Email인데요 이 에노테이션을 가지고 있는 변수, 즉 변수 email의 값은 반드시 이메일 형식을 유지해야 합니다.
ex) hrd@naver.com O hrd@ X
이것이 DTO를 사용하는 두번째 이유입니다. 바로 유효성 검증에 DTO가 용이하기 떄문이죠 @Email 에노테이션 외에도 @Pattenr() 등을 통해 원하는 유효성 조건을 설정할 수 있습니다. 추가로 @Max(), @Min()을 사용하면 변수 값의 최소, 최대 영역 또한 제한할 수 있습니다.
[유효성 검증을 위한 의존 라이브러리 추가]
DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요합니다.
아래와 같이 build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가하세요.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
...
...
}
이제 유효성 검증이라는 개념의 이해를 돕기 위해 email, name phone 변수에 몇가지 조건을 달아보겠습니다.
- email (이메일 주소)
- 값이 비어있지 않거나 공백이 아니어야 합니다.
- 유효한 이메일 주소 형식이어야 합니다
- name (이름)
- 값이 비어있지 않거나 공백이 아니어야 합니다.
- phone (휴대폰 번호)
- 값이 비어있지 않거나 공백이 아니어야 합니다.
- 아래와 같이 010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열이어야 합니다.
- 예) 010-1234-5678
위 제한사항들에 기반한 DTO 클래스는 다음과 같이 변화하게 됩니다.
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class MemberDto {
@NotBlank
@Email
private String email;
@NotBlank(message = "이름은 공백이 아니어야 합니다.")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
변화가 보이시나요?
@NotBlank는 해당하는 변수의 값이 비거나 공백이 아니어야 한다는 조건입니다.
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$")은 정규식 표현으로 "010으로 시작하는 11자리 숫자와 ‘-’로 구성된 문자열"이어야 한다는 조건입니다. 정규식에 대해서는 차후에 자세하게 포스팅하도록 하고 지금은 간단한 설명과 함께 넘어가게 습니다.
- ‘^’은 문자열의 시작을 의미합니다.
- ‘$’는 문자열의 끝을 의미합니다.
- ‘*’는 ‘*’ 앞에서 평가할 대상이 0개 또는 1개 이상인지를 평가합니다.
- ‘\s’는 공백 문자열을 의미합니다.
- ‘\S’ 공백 문자열이 아닌 나머지 문자열을 의미합니다.
- ‘.’은 임의의 문자 하나를 의미합니다.
이제 DTO 클래스는 최종 완성하였습니다. 이제 다시 돌아와 처음에 나왔던 Controller 파일로 돌아가 보도록 하겠습니다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
이제 이렇게 코드를 복잡하게 작성할 필요가 없어졌었죠? DTO를 적용하겠습니다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
...
...
}
DTO 클래스에 유효성 검증 조건들을 추가했기 때문에 Controller 클래스에도 몇가지 에노테이션이 추가되었습니다.
@Valid는 Dto의 유효성 검증을 적용하기 위해 작성하는 에노테이션입니다.
@RequestBody는 JSON 형식의 Request Body를 MemberPostDto 클래스의 객체로 변환을 시켜주는 역할을 합니다.
이 말의 의미는 클라이언트 쪽에서 전송하는 Request Body가 JSON 형식이어야 한다는 말과 같습니다.
만일 JSON 형식이 아닌 다른 형식의 데이터를 전송한다면, Spring 내부에서 ‘Unsupported Media Type’ 과 같은 에러 메시지를 포함한 응답을 전달합니다.
이제 서버를 가동시킨 후, PostMan을 통해 결과를 확인해 보겠습니다.
위처럼 조건에 부합하지 않는 데이터들이 전달되었을 때는 에러 메시지 전송, 다만 중요한 것은 저희가 @RequestBody를 사용했기 때문에 Json 형식으로 전송하여야 합니다.