클라이언트는 브라우저에서 요청을 하고 응답을 받는다.
이때 요청은 서버에서 필요한 데이터를 담아야하고
서버측에서 응답할땐 클라이언트에게 공개할 데이터만 담아서 보내면 된다.
일반 사용자는 그저 클릭하고 타자를 입력할 뿐 요청하는 방법, 응답 받아서 확인하는 방법은 알지 못한다.
일반 사용자가 클릭, 키보드 입력으로 어떤 데이터를 보내게 할지,
데이터를 응답으로 보냈을 때 어떻게 전달할지는 개발자가 해야할 일이다.
요청을 보내고 응답을 보내는데는 엔티티의 모든 데이터가 필요하지 않다.
필요한 정보만 받을 수 있고
클라이언트에게 공개하기 싫은 정보를 제외하고 응답을 보낼 수도 있고,
클라이언트에게 보낼 데이터를 가공할 수도 있다.
요청, 응답을 위해 필요한 데이터를 전달할 객체를 DTO Data Transfer Object 라고 한다.
그래서 Presentation Layer와 Business Layer는 DTO로 데이터를 전달하고
DB 계층에 접근하여 Data를 처리할 때는 Entity를 이용해야한다.
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CategoryDto {
@NotNull
private Long id;
@NotBlank
private String name;
private Long parentId;
public Category toEntity() {
return Category.builder()
.id(id)
.name(name)
.parent(Category.builder().id(getParentId()).build())
.build();
}
public CategoryDto(Category entity){
this.id=entity.getId();
this.name=entity.getName();
if(entity.getParent()!=null&&entity.getParent().getId()!=null) {
this.parentId=entity.getParent().getId();
}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public static class CategorySaveDto {
@NotBlank
private String name;
private Long parentId;
public Category toEntity() {
return Category.builder()
.name(name)
.parent(Category.builder().id(parentId).build())
.build();
}
CategorySaveDto(Category entity){
this.name=entity.getName();
this.parentId=entity.getParent().getId();
}
}
}
카테고리 DTO이다.
카테고리 엔티티는 ParentCategory 객체를 가지지만 DTO는 parentCategoryId를 가진다.
만약 엔티티 그대로 서비스로직을 진행하거나 응답으로 보낸다면
서비스 로직을 수행하기 위해 Category 객체를 생성할때 ParentCategory객체도 필요할 수 있고,
응답을 하는 과정에서 Category 객체의 필드인 ParentCategory 객체에 참조하면서 순환 참조의 늪에 빠질 수 있다.
카테고리 saveDto는 category를 insert 할때 사용할 dto이다.
id는 pk로 auto increase할 것이기 때문에 필요하지 않고 name과 parentId만 있으면 된다.
카테고리 DTO 내부에 innerclass로 saveDTO를 static으로 만들어두면
Category 관련 dto를 CategoryDTO 하나의 클래스내에 정리하여 관리할 수 있다.
그리고 각 dto에는 toEntity와 dto 생성자가 존재하는 이는 entity와 dto사이의 변환을 위한 맵퍼라고 생각하면 된다.
이를 편하게 해주는 MapStruct라는 라이브러리가 존재한다.
이에 관해선 나중에 다뤄보기로 한다.
DTO는 응답을 보낼 데이터를 가공할 수도 있다고 하였다.
public class ProductListOutputDto {//리스트,히트상품,관심,최근상품리스트
private String brand;
private String name;
private String updateTime;
private String pImage;
private Integer totalPrice;
private String totalPriceString;
private Long osId;
@Builder.Default
private List<OptionDto.OptionBasicDto> optionSet = new ArrayList<>();
private Boolean isInterested;
private String optionSetDesc;
public ProductListOutputDto(OptionSet entity) {
this.totalPriceString=new DecimalFormat("#,###").format(entity.getTotalPrice());
this.brand=entity.getProduct().getBrand();
this.name=entity.getProduct().getName();
this.totalPrice = entity.getTotalPrice();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
this.updateTime = entity.getUpdateTime().format(formatter);
this.pImage=entity.getProduct().getImg();
this.osId=entity.getId();
this.optionSet = entity.getOptions().stream().map(t -> new OptionDto.OptionBasicDto(t)).collect(Collectors.toList());
this.isInterested=false;
StringBuilder sb = new StringBuilder();
for (OptionBasicDto option : this.optionSet) {
sb.append(option.getName()+":"+option.getValue());
sb.append("/"); // 나머지 값은 '/'
}
String result = sb.toString();
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1); // 마지막 '/' 제거
}
this.optionSetDesc=result;
}
public ProductListOutputDto(OptionSet entity, String username) {
this.totalPrice = entity.getTotalPrice();
this.brand=entity.getProduct().getBrand();
this.name=entity.getProduct().getName();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
this.updateTime = entity.getUpdateTime().format(formatter);
this.pImage=entity.getProduct().getImg();
this.totalPriceString=new DecimalFormat("#,###").format(entity.getTotalPrice());
this.osId=entity.getId();
this.optionSet = entity.getOptions().stream().map(t -> new OptionDto.OptionBasicDto(t)).collect(Collectors.toList());
this.isInterested=entity.getInterests().stream().anyMatch(t -> t.getMember().getUserName().equals(username));
StringBuilder sb = new StringBuilder();
for (OptionBasicDto option : this.optionSet) {
sb.append(option.getName()+":"+option.getValue());
sb.append("/"); // 나머지 값은 '/'
}
String result = sb.toString();
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1); // 마지막 '/' 제거
}
this.optionSetDesc=result;
}
}
프로덕트 테이블은 brand, name, pImage만을 갖고
옵션셋 테이블은 totalPrice, osId를 가지고
옵션 테이블은 options를 가진다.
하지만 제품 전체 조회 페이지에서 제품의 데이터를 뿌릴때는
이 세 테이블의 컬럼을 가져올뿐만 아니라 날짜 형식을 설정하고
options 의 name과 value들로 Optionset Desc상세설명 String으로 가공한다.
그리고 로그인한 상태인 경우에는 username을 받아 해당 제품이 유저의 관심상품인지 여부도 표시해야한다.
this.isInterested=entity.getInterests().stream().anyMatch(t -> t.getMember().getUserName().equals(username));
스트림을 이용해 제품에 관심상품으로 등록한 username 중에 로그인한 username과 일치하는지를 체크하여 boolean 타입으로 저장한다.
또한 DTO는 다른 DTO들로 구성할 수도 있다.
public class UploadProductDto {
private ProductSaveDto product;
private List<OptionDto> options;
private OptionSetCreateDto optionSet;
}
엔티티와 관련 없이도 필요한 데이터를 객체로 모아서 받을 수도 있다.
public class QueryStringDataDto {
private String orderType;
@NotEmpty
@Builder.Default
private List<OptionDto.OptionNameValueMapDto> optionset=new ArrayList<OptionDto.OptionNameValueMapDto>();
private Integer minPrice;
private Integer maxPrice;
private String nameKeyword;
@NotBlank
private CategoryDto category;
@Builder.Default
private Integer firstResult=0;
}
이 객체는 입력값에 따라 필요한 쿼리를 동적으로 생성하기 위해
정렬기준, 키워드, 카테고리, 옵션, 최소~최대 가격에 관한 데이터를 입력받아 요청할 때 쓰는 DTO 이다.
응답 전용 DTO를 만들수도 있다
public class ResponseDto<T> { //HTTP 응답으로 사용할 DTO
private ProductMsgInterface msg;
private List<T> data;//다른 모델의 DTO도 담을 수 있게 제네릭
//보통 여러개의 데이터를 리스트에 담아 처리하기 때문에 리스트
}
응답을 할때는 기본적으로 Data를 보내고 상태코드나, 상태 메세지를 보낸다.
data는 List<T> 형태로 하면 여러 타입의 객체를 리스트 형태로 받을 수 있어
응답할 때 공용으로 사용하기 좋게 하였고
ProductMsgInterface라는 msg를 갖고 있는데
public enum ProductExceptionMsg implements ProductMsgInterface{
FOUND_NO_MEMBER, FOUND_NO_OPTIONSET,FOUND_NO_OPTIONS, FOUND_NO_PRODUCT,
FOUND_NO_CATEGORY, FOUND_NO_RECENTVIEW,FOUND_NO_INTEREST,
ALREADY_EXISTS_RECENTVIEW,ALREADY_EXISTS_INTEREST, IS_NOT_INTERESTEDD, NEED_LOGIN, WRONG_PARAMETER
}
public enum ProductSuccessMsg implements ProductMsgInterface{
ADD_RECENTVIEW, REMOVE_MY_RECENTVIEWS, REMOVE_RECENTVIEW, REMOVE_OLD_RECENTVIEWS, FIND_MY_RECENTVIEWS,
TOP_CATEGORY, CHILDREN_CATEGORY, ADD_CATEGORY, UPDATE_CATEGORY, REMOVE_CATEGORY,
TAP_HEART, UNTAP_HEART, REMOVE_MY_INTERESTS,
FIND_OPTIONSET_BY_ID, UPLOAD_PRODUCT, FIND_OPTION_NAME_VALUES, FOUND_NO_OTHER_OPTIONSETS, FIND_OTHER_OPTIONSETS, SEARCH_PRODUCTS, UPDATE_OPTIONSET, REMOVE_OPTION, REMOVE_OPTIONSET, REMOVE_PRODUCT, IS_MY_INTEREST, MY_INTERESTS, UPDATE_OPTION
}
ProductExceptionMsg와 ProductSuccessMsg를 만들어 ProductMsgInterface로 상위 캐스팅하였다.
이렇게 하면 예외가 발생할 때는 예외 메세지를 응답DTO에 넣어줄 수 있고
성공했을 때는 성공메세지를 응답DTO에 넣어줄 수 있다.
'Java > Project' 카테고리의 다른 글
스프링부트 프로젝트 CI/CD 환경 구축 (with.AWS + GIthub Actions) (0) | 2023.11.24 |
---|---|
스프링부트 프로젝트 서비스와 예외처리 (1) | 2023.11.22 |
스프링부트 프로젝트 JPA 활용 (0) | 2023.11.21 |
다나가 쇼핑몰 프로젝트 ER diagram과 Entity (0) | 2023.11.20 |
쇼핑몰 웹사이트 제작 프로젝트 (0) | 2023.11.09 |