기존에 작성했던 코드를 보면 Entity와 DTO 구분이 없었다.
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody Member member){
boolean result = memberService.createMember(member);
if(result){
return new ResponseEntity(success(),HttpStatus.OK);
}else{
return new ResponseEntity(failure(),HttpStatus.BAD_REQUEST);
}
}
처음 Spring을 배웠을 땐 그냥 분리해서 사용하다가 Entity를 재활용하면 좋지 않을까? 라는 생각에 이렇게 사용하려 했다.
하지만 이전에 분리해서 사용하던 이유가 궁금해져서 찾아보니 Entity와 DTO는 우선 구분해서 사용하는 것을 권장한다고 한다. 그럼 귀찮고 재활용도 안되지만 DTO를 사용하는 이유를 알아보자.
Entity와 DTO를 구분해야하는 이유
1. Entity 내부 구현을 캡슐화
기존에 나는 Entity가 Getter, Setter를 가지고 있다면 그냥 요청으로도 받고 응답으로도 보내주며 하나의 클래스로 잘 활용하면 되겠다고 생각했다.
여기서 Entity는 도메인의 핵심 로직과 속성을 가지고 있고 DB의 테이블과 매칭되는 클래스이다.
그래서 만약 Entity가 Getter, Setter를 가지게 된다면 비즈니스 로직과 상관없는 곳에서 자원의 속성이 변경될 수도 있다. 또한 Entity를 UI계층에 노출하는 것은 테이블 설계를 화면에 공개하는 것과 마찬가지이므로 보안상으로도 좋지 않은 구조가 된다.
따라서 Entity내부의 구현을 캡슐화 하고 UI계층에 노출시키지 않기 위해서 데이터를 주고 받는 목적으로 DTO를 사용해야 할 이유는 충분히 있는 것이다.
2. 화면에 필요한 데이터만 선별
Entity를 요청과 응답에 사용한다면 응답시 필요하지 않은 속성도 함께 보내진다. 단순히 이름 하나만 보내주고 싶어도 이메일과 함께 보내지는 등 낭비가 생긴다. 만약 서비스 규모가 매우 커져서 Entity 크기가 매우 커진다면 데이터 전송 속도가 느려질 것이다.
@JsonIgnore 어노테이션을 사용하여 보내지 않을 수도 있지만 근본적인 해결책이 될 수는 없다. 실수로 넣어서 보내는 등의 문제가 생길 수 있다.
하지만 특정 API에서 필요한 DTO를 별도로 만들면 해당 API에서 필요한 필드들로만 구성하여 응답 객체를 만들 수 있다. 내 경험상 응답 하나에 DTO클래스 하나를 만드니 파일 찾기도 힘들고 수정하기도 힘들어서 이번에는 이너 클래스를 활용하여 만들 생각이다.
3. 순환참조 예방
JPA로 개발할 때 양방향 참조를 사용했다면 순환 참조를 조심해야 한다.
만약 양방향으로 참조된 Entity를 응답으로 보내면 Entity가 참조하고 있는 객체는 지연 로딩되고 로딩된 객체는 또 다시 본인이 참조하고 있는 객체를 호출하게 되며 순환 참조가 발생하여 문제가생긴다.
근본적인 원인이 양방향 매핑에 있지만 DTO를 사용하면 조금 더 안전하다고 볼 수 있다.
4. Validation 코드와 모델링 코드의 분리
Entity클래스에는 필드에 어노테이션이 많이 붙어있다.
여기에 유효성 관련 어노테이션을 추가하면 복잡해지고 가독성이 떨어진다.
역할에 따라 구분하여 정의한다면 각 클래스들을 각자의 역할에 조금더 집중하여 작성할 수 있다.
결론
Entity클래스 하나로 모든 처리를 하면 개발은 편하게 할 수 있지만 어플리케이션 결함을 얻을 수 있다.
또한 API와 Entity사이에 의존성이 생긴다. 우리는 UI와 도메인이 서로 의존성을 갖지 않고 독립적으로 개발하는 것을 지향한다.
그러니까 하나의 클래스가 하나의 역할을 하도록 Entity와 DTO를 구분하여 사용하자
DTO 생성
dto/MemberDto(member 모듈)
package com.seungh1024.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
public class MemberDto {
@Getter
@Builder
public static class MemberJoinRequest{
private String memberEmail;
private String memberPassword;
private String memberName;
}
@Getter
@Builder
public static class MemberUpdateRequest{
private int memberId;
private String memberPassword;
}
// ===================요청, 응답 구분선 ================
@Getter
@Builder
public static class MemberAllResponse{
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private int memberId;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private String memberEmail;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
private String memberName;
}
}
하나의 응답에 하나의 DTO를 생성하지 않고 Member 관련된 DTO를 이너 클래스로 구현했다.
MemberJoinRequest는 가입할 때 요청을 받을 객체이다.
MemberUpdateRequest는 비밀번호 수정을 할 때 받는 객체이고 이후 로그인을 만들면 memberId는 지워질 듯하다.
MemberAllResponse는 응답 관련 객체로 현재 JsonInclude를 사용하여 여러 응답에서 사용할 수 있게 만들었다.
NON_DEFAULT를 사용한 이유는 NON_NULL로 하니 memberId는 int라서 기본값으로 null이 아닌 0이 들어가서 응답 객체에 항상 포함되었다. 그래서 NON_DEFAULT를 사용하여 값이 들어가지 않은 필드들은 제외하고 응답할 수 있도록 만들었다.
@Getter
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL) //클래스 위에 적용하면 아래 변수들 전부 적용됨
public static class MemberAllResponse{
private Integer memberId;
private String memberEmail;
private String memberName;
private Boolean test = false;
}
위와 같이 클래스 위에 JsonInclude를 사용하면 해당 클래스 내의 변수들 모두 적용이 된다.
그리고 NON_NULL을 사용해도 내가 원하는대로 나오는데 이유는 memberId를 primitive타입인 int가 아니라 wrapper class인 Integer로 선언했기 떄문에 기본 값이 null이 된다.
추가로 나는 큰 고민없이 만들었지만 이런 wrapper class를 Entity만들 때 활용하면 좋다. 만약 Entity를 생성했을 때 DB 테이블에 기본 값으로 null이 들어가길 원한다면 wrapper class로 선언, 그게 아니라 int처럼 0이 들어가고 싶다면 primitive type으로 선언하면 좋다. 다음에는 이런 것도 고민해서 DB 설계를 해보자
controller/MemberController
@PostMapping("/signup")
public ResponseEntity signup(@RequestBody MemberDto.MemberJoinRequest memberDto){
Member member = memberService.createMember(memberDto);
MemberDto.MemberAllResponse response = MemberDto.MemberAllResponse.builder()
.memberId(member.getMemberId())
.memberEmail(member.getMemberEmail())
.memberName(member.getMemberName())
.build();
return new ResponseEntity(success(response),HttpStatus.OK);
}
회원가입 API만 보면 기존의 member를 리턴하던 것을 MemberDto의 이너클래스인 MemberAllResponse를 활용하여 응답을 한 것을 볼 수 있다. 또한 요청객체로 MemberJoinRequest를 사용하였다.
나머지 API들도 위와 같이 바꿔주면된다.
다음에는 진짜 유효성 검사를 추가해야겠당
'Java > Spring boot 프로젝트' 카테고리의 다른 글
Entity 모듈 분리(멀티 모듈 프로젝트) - 게시판 만들기(9) (0) | 2023.03.22 |
---|---|
@Valid를 이용한 객체 유효성 검증 - 게시판 만들기(8) (0) | 2023.03.17 |
@ControllerAdvice를 사용한 예외 처리,에러 핸들링 - 게시판 만들기 (6) (0) | 2023.03.16 |
Spring 공통 응답 만들기(Enum, 제네릭 타입) - 게시판 만들기(5) (0) | 2023.02.20 |
JPA 레포지터리, Member CRUD - 게시판 만들기(4) (0) | 2023.02.20 |