@Embedded
Person
엔티티를 다음과 같이 구성했다.
@Entity
@Getter
@Builder
@AllArgsConstructor
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Gender gender;
// address
private String shi;
private String gu;
private String ro;
private String detail;
private String zipCode;
}
id
를 필드로 갖는다.name
은 사람의 이름이다.gender
은 enum 타입으로, FEMALE
, MALE
, UNSELECTED
의 값을 갖는다.shi
~zipCode
는 이 사람의 주소 정보를 나타낸다.음~ 맘에 안든다. address는 "주소"라는 하나의 개념적 단위인데, 각각의 요소들이 저렇게 드러난게 좀 거슬린다. 더 심각한 문제는 repository에서 발생한다.
public interface PersonRepository {
Optional<Person> findByName(String name);
Optional<Person> findByShi(String shi);
Optional<Person> findByGu(String gu);
Optional<Person> findByRo(String ro);
Optional<Person> findByZipCode(String zipCode);
Person save(Person person);
}
특정 주소를 가진 사용자를 찾고 싶은데 repository를 이렇게 구성하면 변경에 매우매우 취약하고, 캡슐화가 똑바로 안된 코드가 되어버릴 것이다. 각각 요소를 개념적 단위로 묶어서 캡슐화 시켜 코드를 객체지향적으로 바꾸고 싶다.
그래서, DDD에서 말하는 "Value"로 address를 바꾸기로 한다. 밸류는 개념적으로 하나인 데이터를 묶고, 가독성이 좋게 표시하기 위한 수단이다. Person
이 가진 address 정보를 하나의 Address
클래스로 묶어보자.
@Getter
@NoArgsConstructor
@Embeddable
public class Address {
private String shi;
private String gu;
private String ro;
private String detail;
private String zipCode;
}
@Getter
과 @NoArgsController
는 Spring의 message converter가 @RequestBody
로 들어온 JSON 값을 Address
로 매핑하기 위해 필요한 값들이다. 간단히 말하면, 컨버터는 생성자로 객체를 만들고 getter 혹은 setter을 돌며 일치하는 필드명에 리플렉션으로 필드 값을 주입한다.@Embeddable
: Address
가 하나의 개념적 단위인 "밸류"로서 엔티티에 존재하는 복합 필드로 존재할 수 있게 해준다.그러면 Person
의 코드는 어떻게 바뀌었을까?
@Entity
@Getter
@Builder
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Gender gender;
@Embedded
private Address address;
}
기존에 길게 늘어선 address 코드들이 사라졌다. 그리고 이 코드를 읽는 사람들은 Address
의 클래스 이름을 보고 '이 필드 클래스는 주소 정보를 저장하고 있을 것'이라고 쉽게 예측 가능할 것이다.
ddl-auto: create
설정을 하고, h2-console에서 생성된 테이블 이름을 살펴보자. 이렇게 @Embeddable
타입을 이용해도, JPA는 embedded 타입 내부의 필드를 알아서 꺼내 테이블 필드명으로 예쁘게 매칭해준다. 만약 루트 엔티티와 밸류의 property 이름이 겹치게 되면 hibernate 에러가 뜨는데, 그땐 @AttributeOverride
같은걸 써서 임베디드 타입의 필드 이름을 특정 column명으로 override해야한다.
DTO는 이렇게 구성할 수 있다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PersonDto {
private String name;
private Gender gender;
private Address address;
public static PersonDto fromEntity(Person person) {
return PersonDto.builder()
.name(person.getName())
.gender(person.getGender())
.address(person.getAddress())
.build();
}
public Person toEntity() {
return Person.builder()
.name(this.name)
.gender(this.gender)
.address(this.address)
.build();
}
}
@RequestBody
의 JSON은 아래와 같이 구성한다. 메세지 컨버터가 밸류 객체(여기선 Address
)를 예쁘게 바인딩해준다.
{
"name":"부추",
"gender":"FEMALE",
"address": {
"shi":"허리도",
"gu":"가늘군",
"ro":"만지면 부러지리",
"detail": "빌라",
"zipCode":"12345"
}
}
해당 데이터를 요청메세지 본문에 담고 save 요청을 보내면?
굳ㅎㅎ
대강의 프로젝트 ERD가 구성되었다. 각각의 엔티티들이 기본적으로 가질 수 있는 필드들에 대한 설명은 생략하고, application specific한 기능들은..
recipe
와 해당 레시피의 단계들을 나타내는 step
으로 구성된다. 일대다 관계를 가지며, 각각의 recipe
는 여러 개의 재료 ingredient
를 연관 관계로 가진다. 재료들은 재사용될 수 있고, 나중에 커머스 기능 등으로 확장될 수도 있다.recipe
는 여러 개의 tag
를 가진다. tag
는 주요 재료, 음식의 종류, 음식 상황으로 나뉘며 여러 개를 가질 수 있다. 이는 추후 레시피를 검색하는데 활용된다.user
의 랭킹이 업데이트되고 이를 확인할 수 있는 페이지가 존재한다.... 정도를 ERD로 나타내면 아래 사진과 같다.