728x90

클라이언트는 브라우저에서 요청을 하고 응답을 받는다. 

이때 요청은 서버에서 필요한 데이터를 담아야하고 

서버측에서 응답할땐 클라이언트에게 공개할 데이터만 담아서 보내면 된다. 

일반 사용자는 그저 클릭하고 타자를 입력할 뿐 요청하는 방법, 응답 받아서 확인하는 방법은 알지 못한다. 

 

일반 사용자가 클릭, 키보드 입력으로 어떤 데이터를 보내게 할지,

데이터를 응답으로 보냈을 때 어떻게 전달할지는 개발자가 해야할 일이다. 

 

요청을 보내고 응답을 보내는데는 엔티티의 모든 데이터가 필요하지 않다. 

필요한 정보만 받을 수 있고

 

클라이언트에게 공개하기 싫은 정보를 제외하고 응답을 보낼 수도 있고, 

클라이언트에게 보낼 데이터를 가공할 수도 있다. 

 

요청, 응답을 위해 필요한 데이터를 전달할 객체를 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에 넣어줄 수 있다. 

 

 

728x90

Tool : ERDCloud 

https://www.erdcloud.com/

 

ERDCloud

Draw ERD with your team members. All states are shared in real time. And it's FREE. Database modeling tool.

www.erdcloud.com

 

 

프로젝트 전체 테이블

 

 

프로덕트 관련 테이블 

 

Product 테이블 : 제품의 기본 모델에 관한 정보를 담은 테이블

Option 테이블 : 제품이 가질 수 있는 옵션의 정보를 담은 테이블

OptionSet 테이블 : 제품에 옵션들의 정보를 포함하여 하나의 상품으로 간주되어 카트와 주문에 포함될 테이블

Category 테이블 : 카테고리 정보를 담은 테이블

CategorySet 테이블 : 카테고리와 프로덕트를 연결해주는 테이블 

Interest, RecentView 테이블 : 관심상품, 최근 본 상품 테이블 

 

-카테고리 테이블 

카테고리 테이블은 카테고리 이름, 그리고 카테고리 테이블을 셀프 참조하여 부모 카테고리를 가질 수 있어

계층형으로 구성할 수 있게 하였고 

카테고리와 옵션셋 테이블을 다대다 맵핑하여 

하나의 옵션셋은 여러개의 카테고리를 가질 수 있게 하였다. 

 

@Data

@Builder

@AllArgsConstructor

@NoArgsConstructor

@Entity

public class Category {//셀프 참조하는 오너테이블, 카테고리셋과는 종속테이블

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long id; //pk

private String name; //카테고리 이름

 

@JoinColumn(name="parent", nullable = true)

@ManyToOne

@ToString.Exclude

private Category parent; //부모 카테고리

 

@OneToMany(mappedBy = "parent")

@Builder.Default

@ToString.Exclude

private List<Category> childTypes= new ArrayList(); //자식 카테고리들

 

@OneToMany(mappedBy = "category")

@Builder.Default

@ToString.Exclude

private List<CategorySet> categorySets = new ArrayList<>();

//다대다 맵핑을 위한 categorySet과 관계 설정

}

 

-프로덕트 테이블 

public class Product extends BaseEntity {//제품의 기본 모델 정보

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long id;//pk

private String name;//제품명

private String brand;//브랜드

private Integer price;//기본 가격

private String descImage;//설명 이미지 파일

private String prevImage;//디테일이미지

private String img;//제품 이미지

 

@OneToMany(mappedBy = "product",fetch = FetchType.EAGER)

@Builder.Default

private List<CategorySet> categorySets = new ArrayList<>();

//하나의 제품은 부모카테고리, 자식카테고리 여러개를 가질 수 있다.

//예를 들어, 컴퓨터, 일체형PC, 브랜드PC ...

 

@ToString.Exclude

@OneToMany(mappedBy = "product",cascade = {CascadeType.REMOVE,CascadeType.PERSIST},orphanRemoval = true)

@Builder.Default

private List<OptionSet> optionSets = new ArrayList<>();

 

}

여기서 Product가 상속받은 BaseEntity는 createTime과 updateTime을 가진 엔터티로 

@MappedSuperclass

public class BaseEntity {

@CreationTimestamp

@Column(updatable = false)

@ToString.Exclude

private LocalDateTime createTime;//데이터 생성시간

@UpdateTimestamp

private LocalDateTime updateTime;//데이터 갱신시간

}

데이터의 관리를 위해 필요한 기본적인 정보를 가지는 superclass로 다른 엔터티들이 상속받아 사용할 수 있게 

@MappedSuperclass 어노테이션을 사용해 테이블로 생성되지 않고 상속하는 역할만 할 수 있게 하였다. 

 

 

프로덕트는 제품의 기본 모델에 관한 정보를 담은 테이블로 제조사, 제품 이미지, 기본 가격 등에 관한 정보를 담고 있다. 

프로덕트 테이블에 대해 고민을 많이 했는데 

컴퓨터와 같이 하나의 모델에 대해서도 다양한 옵션이 선택될 수 있고 선택된 옵션에 따라 가격이 변동 될 수 있기에 

기본 모델을 product로 하고 거기에 options을 더한 optionset을 하나의 완전한 상품으로 구상하였다. 

 

그래서 cart, orderitem, 관심상품, 최근 본 상품 등에 들어갈 제품은 product가 아닌 optionset이 되고, 

optionset이 재고, 판매량, 조회수와 같은 정보를 갖게 된다. 

실제로 제품 상세 페이지도 product가 아닌 optionset의 상세 페이지가 된다. 

 

 

public class Options {//옵션셋FK를 가지는 오너테이블

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long id;//pk

 

private String name; //옵션명

private String value; //옵션값

private Integer extraPrice;

//해당 옵션이 옵션셋에 등록될 경우 프로덕트의 총 가격에 추가금

@ManyToOne(fetch = FetchType.EAGER)

@JoinColumn(name = "optionSetId")

@ToString.Exclude

private OptionSet optionSet;//옵션셋 FK

}

 

여기서 옵션과 옵션셋 테이블을 한번 더 분리한 이유는 

컴퓨터만을 product의 대상으로 삼고 있지 않고 다양한 전자제품을 대상으로 하기 때문에 

options가 셀 수 없이 많아질 수밖에 없고 만약 optionset에 그 많은 options들을 nullable 컬럼으로 만들어 두는 것은 매우 비효율적이라고 생각했다. 

그래서 옵션 설정에 자유도를 높이기 위해 options 테이블을 분리하였다. 

 

옵션셋에 totalPrice를 두는 것에 관해서 많은 고민을 했었다. 

원래는 기본 product의 price에 options의 extraPrice들을 모두 더해 나온 값을 표시하면 된다고 생각했는데 

제품 리스트를 조회할 때마다

매 상품마다 options를 모두 뽑아 가격 연산하는 과정을 거치는 작업은 올바른 작업은 아니라고 판단하였고 

옵션셋에 총가격 컬럼을 하나 추가하는 편이 훨씬 경제적이라고 판단하여

제품을 insert 하는 과정에서 extraPrice들을 모두 더해 optionset의 totalPrice의 컬럼에 값을 대입해주기로 하였다. 

 

 

- 관심상품, 최근 본 상품 

관심상품과 최근 본 상품은 멤버와 옵션셋을 참조키로 가진다. 그리고 최근 본 상품에는 30일 이후 자동으로 삭제되는 서비스를 구현한다. 

 

@Entity

@Data

@AllArgsConstructor

@NoArgsConstructor

@Builder

@EqualsAndHashCode(callSuper = true)

@ToString(callSuper = true)

@Table(name = "interest", uniqueConstraints = @UniqueConstraint(columnNames = {"memberId","optionSetId"}))

public class Interest extends BaseEntity{//관심상품

//유저와 옵션셋을 이어주는 중간테이블

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long id;//pk

 

@JoinColumn(name = "memberId")

@ManyToOne

private Member member;// 유저FK

 

@JoinColumn(name = "optionSetId")

@ManyToOne

@ToString.Exclude

private OptionSet optionSet;// 옵션셋FK

 

}

 

관심상품과 최근 본 상품은 멤버와 옵션셋의 복합키로써 유니크 제약조건을 가져야한다.

728x90

프로젝트 기간: 2023 - 10 - 16 ~ 2023 - 11 - 11

레퍼런스 : 다나와 https://www.danawa.com/

 

스마트한 쇼핑검색, 다나와! : 가격비교 사이트

가격비교 사이트 - 온라인 쇼핑몰, 소셜커머스 전 상품 정보 가격비교 사이트, 비교하면 다나와

www.danawa.com

인원 : 8명

GitHub 주소 https://github.com/choliea/danaga

 

GitHub - choliea/danaga: final project Danaga shopping mall By team.Avengers 2023-10-16

final project Danaga shopping mall By team.Avengers 2023-10-16 - GitHub - choliea/danaga: final project Danaga shopping mall By team.Avengers 2023-10-16

github.com

파트 : 프로덕트 

사용 언어 : 자바, 자바스크립트

 

 

사용 기술 스택 : 

스프링 부트를 활용한 쇼핑몰 웹 사이트 제작 프로젝트 

 

ERD

 

 

Product part Table

 

 

https://youarethebestcoding.tistory.com/128

 

다나가 쇼핑몰 프로젝트 ER diagram과 Entity

Tool : ERDCloud https://www.erdcloud.com/ ERDCloud Draw ERD with your team members. All states are shared in real time. And it's FREE. Database modeling tool. www.erdcloud.com 프로젝트 전체 테이블 프로덕트 관련 테이블 Product 테이블 :

youarethebestcoding.tistory.com

 

 

구현한 기능 및 서비스 

카테고리 선택시 해당 카테고리의 제품들이 가지고 있는 스펙들을 불러와 조건 검색을 할 수 있다. 

카테고리 선택, 옵션 선택, 정렬 기준 선택, 검색 버튼 클릭시마다 검색하여 제품들을 보여준다. 

 

옵션 선택하여 조건 검색 

같은 종류의 조건에 대하여는 합연산, 다른 종류의 조건에 대하여는 곱연산 적용

 

 

예를들어, 운영체제의 윈도우11과 윈동우10을 동시에 선택하면 

윈도우11 또는 윈도우10인 제품을 검색하고 

운영체제의 윈도우11, 화면비율 16:9 를 선택하면

윈도우11이면서 화면비율 16:9인 제품을 검색한다. 

 

제품명 검색은 대소문자 구분하지 않고 검색한다. 

 

https://youarethebestcoding.tistory.com/130

 

스프링부트 프로젝트 JPA 활용

JPA에 대해서 다룬 포스트가 있다. https://youarethebestcoding.tistory.com/115 Spring Data JPA begins http://projects.spring.io/spring-data/ Spring Data Spring Data’s mission is to provide a familiar and consistent, Spring-based programming mod

youarethebestcoding.tistory.com

 

 

페이징은 무한스크롤API를 이용하였다. 

https://youarethebestcoding.tistory.com/133

 

Intersection Observer 무한스크롤 구현하기

#무한스크롤 기존 페이징 방식에서 벗어나 최근 떠오르는 페이징 방식이다. 특정 개수만큼만 로딩하고 특정 뷰포트를 넘어가면 다음 아이템을 로딩하는 방식으로 비동기 방식으로 html을 누적

youarethebestcoding.tistory.com

 

 

 

 

프로덕트 검색 전체 시연

 

제품 상세페이지 

제품 상세페이지에서는 해당 제품과 동일한 모델의 다른 옵션으로 구성된 제품을 선택할 수 있고

선택시 해당 제품의 상세 페이지로 전환된다.

 

DESCRIPTION 탭에서는 제품의 설명 이미지 파일을 로딩하고 

SPECIFICAION 탭에서는 제품의 상세 스펙을 표로 보여준다. 

 

로그인 상태에서 제품의 하트를 클릭하면 Toast 메세지와 함께 관심상품에 등록된다. 

 

제품 상세 페이지에 들어가본 제품은 최근 본 상품에 등록되고 

로그인 하지 않은 상태에서 본 제품도 로그인 후 최근 본 상품에 등록되게 된다. 

 

최근 본 상품은 등록된지 30일이 지나는 시점에 자동으로 삭제된다. 

 

 

배포 

AWS CLI를 이용해 elasticbeanstalk 환경을 구축하고 github action을 이용해 

 

무중단 자동 배포 환경 구축 

배포된 사이트 index 페이지 

 

https://youarethebestcoding.tistory.com/134

 

스프링부트 프로젝트 CI/CD 환경 구축 (with.AWS + GIthub Actions)

이번 프로젝트에서는 AWS의 ElasticBeanstalk 을 이용해 application을 배포하고 github actions를 이용해 aws에 무중단 배포할 수 있는 환경을 구축하였다. AWS의 서비스에 관해서는 이미 다룬 적이 있으니 생

youarethebestcoding.tistory.com

 

 

 

 

프로젝트 관련 포스트 

https://youarethebestcoding.tistory.com/129

 

스프링부트 프로젝트 계층에 따른 데이터 전송 형태

클라이언트는 브라우저에서 요청을 하고 응답을 받는다. 이때 요청은 서버에서 필요한 데이터를 담아야하고 서버측에서 응답할땐 클라이언트에게 공개할 데이터만 담아서 보내면 된다. 일반

youarethebestcoding.tistory.com

 

 

https://youarethebestcoding.tistory.com/131 

 

스프링부트 프로젝트 서비스와 예외처리

이번 프로젝트를 하면서 예외 처리에 대해서 고민 많았고 실제로 작업하면서 몇번씩 수정을 거치게 되었다. 일단 예외가 발생했을 때 발생원인이 명확한 경우 구체적인 예외 사유를 알 수 있게

youarethebestcoding.tistory.com

 

https://youarethebestcoding.tistory.com/132

 

Thymeleaf Layout

#타임리프 레이아웃 코드의 재사용이 가능한 부분을 템플릿화할 수 있게 도와주는 타임리프 라이브러리 #레이아웃을 사용하는 이유 타임리프의 insert나 replace 기능은 많이 사용하지만 이 기능에

youarethebestcoding.tistory.com

 

728x90

Entity 공통으로 포함할 createdAt, updatedAt 필드를 

BaseEntity를 만들어 다른 Entity들이 상속할 수 있게 SuperClass로 사용

@Data

@MappedSuperclass

public class BaseEntity {

@CreationTimestamp

@Column(updatable = false)

private LocalDateTime createdAt;

@UpdateTimestamp

private LocalDateTime updatedAt;

}

entity 관리를 위해 entity 생성, 수정 시간을 기록하는 createAt, updatedAt을 가진다. 

ToString, HashCode, Equals를 포함하는 Data 어노테이션과 

BaseEntity 테이블을 따로 만들지 않고 superclass로 하여 자식 클래스들이 필드만 사용할 수 있게 하기 위해 

@MappedSuperclass 어노테이션을 사용한다. 

 

@Data

@EqualsAndHashCode(callSuper = true)

@ToString(callSuper=true)

@AllArgsConstructor

@NoArgsConstructor

@Builder

@Entity

public class ProductDetail extends BaseEntity {

ProductDetail 엔터티는 BaseEntity를 상속하고 

superclass(BaseEntity)의 @Data 어노테이션(Equals,HashCode,ToString)을 사용하기위해 

@EqualsAndHashCode,ToString 어노테이션에 callSuper 속성을 true로 한다. 

 

 

product와 productDetail은 1:1 관계이고 

ProductDetail이 Product_id를 FK로 갖고 있으므로 OwnerTable이라 할 수 있다. 

 

public class Product extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productId;

 

private String name;

private Integer price;

private Integer stock;

 

 

//1:1 (Owner table 아님)

@OneToOne(mappedBy = "product", cascade = CascadeType.PERSIST)

private ProductDetail productDetail;

 

Product는 ProductDetail 객체를 가지고 

1:1 맵핑이므로 @OneToOne 어노테이션을 붙인다. 

 

OwnerTable이 아닌 클래스에서 OwnerTable 의 클래스를 mapping할 때 속성으로 

mappedBy를 작성해줘야 한다. 

Product가 가지고 있는 ProductDetail은 Product에 의해 mapping 되므로 mappedBy product 

 

cascade는 product를 작업할때 product가 가진 productDetail도 함께 작업할지에 대한 옵션이다. 

all, detach, merge, persist, refresh, remove가 있고 persist는 영속성에 추가할 때 관련 엔터티도 함께 추가된다. 

 

public class ProductDetail extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productDetailId;

private String description;

 

/*

* 1:1 OWNER TABLE 참조키를 갖고 있어서

*/

@OneToOne(cascade = CascadeType.PERSIST)//save할 때 연관된 프로덕트도 같이 하겠다.

@JoinColumn(name="product_id")

@ToString.Exclude

private Product product;//tostring시 product의 detail의 product의 detail 순환참조 stackoverflow

 

 

ProductDetail은 Product를 FK로 가지고 있으므로 OwnerTable이라고 할 수 있다. 

마찬가지로 @OneToOne 어노테이션을 사용하고 cascade 설정을 줄 수 있다. 

오너테이블에서 FK 엔터티를 맵핑할때는 mappedBy 속성이 필요없다. 

대신 @JoinColumn 어노테이션을 작성해줄 수 있다. (default로 해주긴함)

JoinColumn 은 default로 name="FK" 를 가진다. 

JoinColumn의 이름은 productDetail 테이블에서 product를 컬럼으로 가져올때 가질 물리적 이름이다. 

productDetail 테이블에서 Product엔터티는 product_id 라는 FK로 가져가기 때문에 name="product_id"

관례상 보통 모든 엔터티의 PK(Id)는 [entityName]Id 가 아닌 "Id"로 동일하게 주더라도 

JPA에서 productDetail에 product는 product_id로 설정해준다. 

 

만약 ProductDetail에서 Product를, 혹은 Product에서 ProductDetail를 참조하여 ToString을 호출하면 

Product의 ToString은 product가 가진 ProductDetail을 ToString하다가 또 Product를 만나 Product를 ToString 하고 

순환참조하다 stackOverflow에 빠질 것이다. 

이를 막기 위해서 

@ToString.Exclude 어노테이션을 하면 ProductDetail에서 ToString 하는 과정에서 Product를 제외하고 ToString 하기 때문에 순환참조를 막을 수 있다. 

 

ProductDetail Save() 

ProductDetail(Owner Table)에서 save 할때 

productDetail이 가지는 product를 set 해주면 

알아서 product에도 product가 가지는 productDetail을 set해준다. 

Product product = Product.builder().name("name").price(1111).stock(111).build();

ProductDetail detail = ProductDetail.builder().description("des").build();

//연관관계설정

detail.setProduct(product);

productDetailRepository.save(detail);

 

>>> ProductDetail->ProductProduct(super=BaseEntity(createdAt=2023-10-11T18:01:41.229506, updatedAt=2023-10-11T18:01:41.229506), productId=2, name=name, price=1111, stock=111, productDetail=ProductDetail(super=BaseEntity(createdAt=2023-10-11T18:01:41.247503, updatedAt=2023-10-11T18:01:41.247503), productDetailId=1, description=des))

 

Product테이블 save 결과

 

ProductDetail 테이블 save 결과 

 

productDetail는 product를 가지기 때문에 productDetail에 product를 set 하여야 하고 

product는 그렇지 않아도 상관없다. 

productDetail의 product에 cascade persist가 적용되어 product도 함께 save 되었다. 

 

반면 

void productWithProductDetailSaveAndRead() {

 

ProductDetail productDetail=ProductDetail.builder().description("desc").build();

 

Product product=Product.builder().name("name").price(60000).stock(300).build();

/*

* 연관관계설정(OWNER테이블아닌경우)

* Product-->ProductDetail

*/

product.setProductDetail(productDetail);

productDetail.setProduct(product);

productRepository.save(product);

}

오너테이블이 아닌 product에서 product를 함께 save 하기 위해서는 

product는 productDetail을 set 해야하고 

productDetail은 product를 set 해야한다. 

 

만약 

product.setProductDetail(productDetail);

// productDetail.setProduct(product);

productRepository.save(product);

productDetail에 product를 set하지 않으면 

productDetail에 product_id가 null로 된 것을 확인할 수 있다. 

 

Provider와 Product는 1:N 관계이다. 

//1:n(owner table 아님)

@Builder.Default//초기값 주기

@OneToMany(mappedBy = "provider",cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)

private List<Product> products=new ArrayList<>();

Provider(One) Product(Many)이므로 

Provider의 Product는 컬렉션으로 가지고 OneToMany 어노테이션을 사용한다. 

provider는  ownerTable이 아니므로 mappedBy 속성을 부여한다. 

Builder.Default 값은 

Provider클래스의 List<Product>에 new ArrayList<>() 를 초기값을 추기 위해 붙여주는 어노테이션이다. 

 

fetch type이란

Jpa가 Entity를 조회할 때 연관관계에 있는 객체들을 전부 접근할지, 필요할때만 접근할지에 대한 설정이다. 

default는 최적화를 위해 Lazy이고 

연관 관계에 있는 Entity를 가져오지 않고, getter로 접근하는 경우만 가져온다. 

Eager는 항상 연관관계에 있는 Entity를 모두 가져온다. 

 

 

Product는 Provider를 

//n:1 Owner Table

@ManyToOne(cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)//default : lazy

@JoinColumn(name="provider_id")

@ToString.Exclude

private Provider provider;

 

ManyToOne으로 매핑한다. 

 

 

Product product = Product.builder().name("name").price(10000).stock(100).build();

Product product2 = Product.builder().name("name2").price(20000).stock(200).build();

Product product3 = Product.builder().name("name3").price(30000).stock(300).build();

Provider provider = Provider.builder().name("name").build();

product.setProvider(provider);

product2.setProvider(provider);

product3.setProvider(provider);

List<Product> products = provider.getProducts();

products.add(product);

products.add(product2);

products.add(product3);

provider.setProducts(products);

 

providerRepository.save(provider);

product1,2,3, provider를 생성하고 

product들에 provider를 set하고 

Provider의 Products를 가져온다. 

Products는 초기값으로 new ArrayList<>()를 갖고 있다. 

Provider의 products에 product를 add 하고 setProducts 한 후 provider를 save한다. 

한 transaction 안에 (@Transactional)있다면 products.add(product1,2,3) 만 해주고 provider에 다시 setProducts 하지 않아도 context에서 확인하고 save 처리해준다. 

 

 

반대로 Product에서는 

Provider provider=Provider.builder()

.name("name")

.build();

 

Product product=Product.builder()

.name("name")

.price(10000)

.stock(100)

.build();

/***** 연관설정 Product-->Provider *****/

product.setProvider(provider);

productRepository.save(product);

product에  provider만 set해줘도 된다. 

 

 

category엔터티는 

@Column(unique = true,nullable = false)

private String code;

code 컬럼을 unique로 갖고 있다. 

@Column에 unique, nullable 속성을 추가해준다. 

 

@OneToMany(cascade = {CascadeType.ALL},orphanRemoval = true,mappedBy = "category",fetch = FetchType.EAGER)

@Builder.Default

private List<Product> products=new ArrayList<>();

 

orphanRemoval 속성은 자식 엔터티가 고아가 되면 삭제해주는 속성이다. 

이렇게 하면 

findCategory.getProducts().clear();

카테고리에서 product리스트를 지우면 카테고리에서 프로덕트를 갖지 않게되므로 쫓겨난 프로덕트는 고아가 된다. 

그럼 그 고아엔터티를 삭제해주어 product테이블에서도 삭제된다. 

 

'Java > Spring Boot' 카테고리의 다른 글

Thymeleaf Layout  (1) 2023.11.24
JPA 계층형 게시판  (0) 2023.10.10
Spring Data JPA begins  (0) 2023.10.06
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14

+ Recent posts