JPA에서 Embedded활용하기

박민주·2024년 2월 1일

spring-JPA

목록 보기
2/5

String과, int같은 약간 Raw한 데이터 타입으로 매핑한다음 서비스로직에서 해당 값들을 변환하거나 기능적으로는 문제가 없을텐데 converter를 통해서 엔티티로 매핑 시켰을까? 코드의 가독성측면이 가장 크다.

임베디드 타입

새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입(embedded type)이라 한다.

임베디드 타입으로 가장많이 사용할 수 있는 영역들은 가격이 있다.
가격은 공급가 + 부가세 = 총 금액으로 나타낼 수 있고, 주소도 임베디드 타입으로 사용하기에 적합하다. 주소는 시 + 군(구) + 상세주소 + 우편번호로 나타낼 수 있다. 그렇다면 예시를 들어 사용해보겠다.

사용자의 정보를 저장하는 Member

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;
}

유저 정보의 변경을 저장하는 MemberHistory

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 와 같은 주소관련 필드가 중복된다. 코드는 복사 붙여넣기를 지양하고 객체화 하는것이 객체지향 측면에서도 맞다.

주소를 저장할 임베디드 타입의 Address

@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; // 우편번호
}

Member, MemberHistory

private String city;
private String district;
private String detail; 
private String zipCode; 
-------------변 경---------------
@Embedded
private Address homeAddress;

@Embedded는 값 타입을 사용하는 곳에 표시해야 한다.

기존의 컬럼들을 복사해서 히스토리에 넣었는데 address라는 클래스로 묶어서 표현할 수있게 되었다.

Test 및 실행결과

	@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 및 실행결과

 	@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가 붙은게 오히려 지저분해 보이기도 한다. 차라리 객체를 생성해서 사용하는게 나을 수도 있다. 이건 상황에 따라 적절히 선택 해야 할것이다.

만일 임베디드 타입에 null이 들어간다면

만일 Address가 null이면 어떻게 처리가 될까? NullPointerException이 발생해서 문제가 될거같기도 하다. 어떻게 처리 될까?

Test 및 실행결과

 		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]
profile
개발자 되고싶다..

0개의 댓글