String과, int같은 약간 Raw한 데이터 타입으로 매핑한다음 서비스로직에서 해당 값들을 변환하거나 기능적으로는 문제가 없을텐데 converter를 통해서 엔티티로 매핑 시켰을까? 코드의 가독성측면이 가장 크다.
새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입(embedded type)이라 한다.
임베디드 타입으로 가장많이 사용할 수 있는 영역들은 가격이 있다.
가격은 공급가 + 부가세 = 총 금액으로 나타낼 수 있고, 주소도 임베디드 타입으로 사용하기에 적합하다. 주소는 시 + 군(구) + 상세주소 + 우편번호로 나타낼 수 있다. 그렇다면 예시를 들어 사용해보겠다.
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
@Enumerated(value = EnumType.STRING)
private Gender gender;
private String city;
private String district;
private String detail;
private String zipCode;
}
public class MemberHistory extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@Enumerated(value = EnumType.STRING)
private Gender gender;
private String city;
private String district;
private String detail;
private String zipCode;
@ManyToOne
@ToString.Exclude
private Member member;
}
하지만 city, district 와 같은 주소관련 필드가 중복된다. 코드는 복사 붙여넣기를 지양하고 객체화 하는것이 객체지향 측면에서도 맞다.
@Embeddable는 값 타입을 정의하는 곳에 표시한다.
@Embeddable
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Address {
private String city; // 시, Enum으로 한다면 정규화되엇 저장할 수있을것이다.
private String district; // 구
@Column(name = "address_detail")
// detail이란 필드명으론 오해가 생길 수 있어 Column을 따로지정해주었다.
private String detail; // 상세주소
private String zipCode; // 우편번호
}
private String city;
private String district;
private String detail;
private String zipCode;
-------------변 경---------------
@Embedded
private Address homeAddress;
@Embedded는 값 타입을 사용하는 곳에 표시해야 한다.
기존의 컬럼들을 복사해서 히스토리에 넣었는데 address라는 클래스로 묶어서 표현할 수있게 되었다.
@Test
void embedTest(){
memberRepository.findAll().forEach(System.out::println);
Member member = new Member();
member.setName("steve");
member.setAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06251"));
memberRepository.save(member);
}
Member(super=BaseEntity(createdAt=2024-02-01T17:52:22.323627100, updatedAt=2024-02-01T17:52:22.323627100),
id=6, name=steve, email=null, gender=null,
address=Address(city=서울시, district=강남구, detail=강남대로 364 미왕빌딩, zipCode=06251))
하지만 주소라면 하나만 있는것이 아니라 자택주소, 회사주소 등 그 이상의 값을 저장할 수있다.
만약 Address라는 값이 없다면 homeCity, homeDistrict, CompanyCity..와 같이 선언되어야 할것이고 MemberHistory에도 동일하게 선언해야 할것이다.
@Embeddable로 재활용해보겠다.
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "home_city")),
@AttributeOverride(name = "district", column = @Column(name = "home_district")),
@AttributeOverride(name = "detail", column = @Column(name = "home_address_detail")),
@AttributeOverride(name = "zipCode", column = @Column(name = "home_zip_code"))
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "company_city")),
@AttributeOverride(name = "district", column = @Column(name = "company_district")),
@AttributeOverride(name = "detail", column = @Column(name = "company_address_detail")),
@AttributeOverride(name = "zipCode", column = @Column(name = "company_zip_code"))
})
private Address companyAddress;
하지만 같은 타입의 컬럼은 쓸 수 없기에 @AttributeOverrides로 재정의 해줘야 한다. @AttributeOverride로 컬럼들의 이름을 정해준다.
@Test
void embedTest(){
memberRepository.findAll().forEach(System.out::println);
Member member = new Member();
member.setName("steve");
// member.setAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06251"));
member.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06251"));
member.setCompanyAddress(new Address("서울시", "성동구", "성수이로 113 제강빌딩", "04794"));
memberRepository.save(member);
memberRepository.findAll().forEach(System.out::println);
}
Member(super=BaseEntity(createdAt=2024-02-01T17:59:22.484068200, updatedAt=2024-02-01T17:59:22.484068200),
id=6, name=steve, email=null, gender=null,
homeAddress=Address(city=서울시, district=강남구, detail=강남대로 364 미왕빌딩, zipCode=06251),
companyAddress=Address(city=서울시, district=성동구, detail=성수이로 113 제강빌딩, zipCode=04794))
객체를 재활용해서 사용하는 측면에서 나빠보이지 않지만 @AttributeOverride가 붙은게 오히려 지저분해 보이기도 한다. 차라리 객체를 생성해서 사용하는게 나을 수도 있다. 이건 상황에 따라 적절히 선택 해야 할것이다.
만일 Address가 null이면 어떻게 처리가 될까? NullPointerException이 발생해서 문제가 될거같기도 하다. 어떻게 처리 될까?
Member member1 = new Member();
member1.setName("joshua");
member1.setHomeAddress(null);
member1.setCompanyAddress(null);
memberRepository.save(member1);
Member member2 = new Member();
member2.setName("jordan");
member2.setHomeAddress(new Address());
member2.setCompanyAddress(new Address());
memberRepository.save(member2);
memberRepository.findAll().forEach(System.out::println);
Member(super=BaseEntity(createdAt=2024-02-01T17:59:22.542060900, updatedAt=2024-02-01T17:59:22.542060900),
id=7, name=joshua, email=null, gender=null,
homeAddress=null, companyAddress=null)
Member(super=BaseEntity(createdAt=2024-02-01T17:59:22.549059300, updatedAt=2024-02-01T17:59:22.549059300),
id=8, name=jordan, email=null, gender=null,
homeAddress=Address(city=null, district=null, detail=null, zipCode=null),
companyAddress=Address(city=null, district=null, detail=null, zipCode=null))
약간 다른거 같다. 객체가 null인 것과 객체 안의 컬럼이 모두 null인 두가지의 상황처럼 보인다. 하지만 그건 영속성 컨텍스트가 제공하고 있는 캐시때문이다. 사실 db에는 임베드된 객체가 null인 경우와 내부 모든 컬럼이 null인 경우 모두 같게 저장된다.
[2024-02-01 18:05:28.439913, 7, 2024-02-01 18:05:28.439913, null, null, null, null, null, null, null, null, null, joshua, null]
[2024-02-01 18:05:28.445913, 8, 2024-02-01 18:05:28.445913, null, null, null, null, null, null, null, null, null, jordan, null]