Dev/JPA

JPA Embedded, Embeddable, 속성의 재정의

OK-가자 2022. 3. 2. 17:31

JPA에서 Embedded 활용하기.

🤔🤔 컨버터를 통해 자바객체에 맵핑하는 법을 배웠다. String이나 Int 같은 Row한 값으로 받아서 서비스 로직에서 변환해도 문제가 없을탠데 왜 이렇게 까지 할까?
바로 코드의 가독성 때문이다.
오픈소스나 기업체 상용소스같은 최근코드는 이해하기쉽고 읽기쉽게 가독성이 좋게 작성하는것이 트랜드이다. 그렇기에 Entity에 Embed클래스를 한번 사용해보자.
현업에서 Enbedded타입을 가장 많이 쓰는 예로써, 주문시의 가격 필드가 있다. 가격은 공급가 부가세 토탈가격 같은 가격필드는 상품 도메인이나, 주문 도메인, 결제, 정산 등등 여러가지 필드에 쓰이기 때문에 적합하다.
또한 주소정보도 적합하다. 주소정보를 String address에 모두 저장할 수 있지만, 그렇게 되면 값 자체가 정규화 되지 않아 서울, 서울시, 서울특별시 이런식으로 Data가 정리안될수 있다. 정규화해서 Data를 선택할수있게 하면 비표준화된 정보를 정리하여 생성할수 있다. 주소 값은 도 시 군.구 우편번호 등등 으로 나눈다. 주소를 예로 만들어보자.

Embedded

public class User extends BaseEntity {
    private String city;
    private String district;
    private String detail;
    private String zipCode;
    ...
    ...
    ...
{
  • 이런식으로 필드를 선언하고, 다른 엔티티에서도 쓰고 하면 DRY 원칙에 위배된다. 그래서 Embedded타입 클래스를 만들어 쓴다.
@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    private String city;        // 시
    private String district;    // 구
    private String detail;      // 상세주소
    private String zipCode;     // 우편번호
}
public class User extends BaseEntity {
...
...
...
    @Embedded
    private Address address;
...
...

{
  • userHistory도 똑같이 변경하고 UserEntityListener에도 Address를 set한다.

    public class UserEntityListener {
          ...
          ...
          userHistory.setAddress(user.getAddress());
    
          userHistoryRepository.save(userHistory);
      }
    }
    
- TEST
@Test
void embedTest() {
    userRepository.findAll().forEach(System.out::println);

    User user = new User();
    user.setName("steve");
    user.setAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06241"));

    userRepository.save(user);


    userRepository.findAll().forEach(System.out::println);
    userHistoryRepository.findAll().forEach(System.out::println);

}
- 이렇게 기존 컬럼을 복사 붙여넣기 보다 Address 클래스로 묶어서 사용할수 있게 되었다.
- 여기서 서로 다른 Address 값도 사용 가능하다. 예를 들어 HomeAddress나 companyAddress 같은 경우 말이다.

public class User extends BaseEntity {
..
.
.
@Embedded
private Address homeAddress;

@Embedded
private Address companyAddress;

.
.
.

}

- 이렇게 사용 가능할까? 
@Test
void embedTest() {
        user.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06241"));
    user.setCompanyAddress(new Address("서울시", "성동구", "성수이로 113 제강빌딩", "04794"));

.
.
{

-  Repeated column in mapping for entity 오류가 날 것이다. Address를 두번씩 사용해서 내부 컬럼이 두번사용된것이다. DB에서는 똑같은 컬럼이 2번 사용된 것이다. 그래서 @AttributeOverrides사용한다.

### @AttributeOverrides
- 동일인 Address를 사용하지만 어노테이션을 사용해서 값들을 재정의 하겠다는 뜻이다.
@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;
- 사실 Entity가 지저분해 질수도 있다. 객체를 하나 새로 선언하고 안쓸지, 조금 지저분하지만 Embbed클래스 쓸지는 개발자가 선택하면 된다.
- 만약 Address가 null로 아무것도 안들어가면 어떻게 될까? Test 하기 위해 UserRepository에 @Query 하나 만들고 테스트 하자.
@Query(value = "select * from user", nativeQuery = true)
List<Map<String, Object>> findAllRawRecord();
@Test
void embedTest() {
    userRepository.findAll().forEach(System.out::println);

    User user = new User();
    user.setName("steve");
    user.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06241"));
    user.setCompanyAddress(new Address("서울시", "성동구", "성수이로 113 제강빌딩", "04794"));

    userRepository.save(user);

    User user1 = new User();
    user1.setName("joshua");
    user1.setHomeAddress(null);
    user1.setCompanyAddress(null);

    userRepository.save(user1);

    User user2 = new User();
    user2.setName("jordan");
    user2.setHomeAddress(new Address());
    user2.setCompanyAddress(new Address());

    userRepository.save(user2);

    userRepository.findAll().forEach(System.out::println);
    userHistoryRepository.findAll().forEach(System.out::println);

    userRepository.findAllRawRecord().forEach(a -> System.out.println(a.values()));
}

UserHistory(super=BaseEntity(createdAt=2022-02-16T18:43:54.365, updatedAt=2022-02-16T18:43:54.380), id=2, name=joshua, email=null, gender=null, homeAddress=null, companyAddress=null, user=User(super=BaseEntity(createdAt=2022-02-16T18:43:54.365, updatedAt=2022-02-16T18:43:54.365), id=7, name=joshua, email=null, gender=null, homeAddress=null, companyAddress=null))
UserHistory(super=BaseEntity(createdAt=2022-02-16T18:43:54.365, updatedAt=2022-02-16T18:43:54.380), id=3, 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), user=User(super=BaseEntity(createdAt=2022-02-16T18:43:54.365, updatedAt=2022-02-16T18:43:54.365), 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)))

- 같은 빈 값인데 다르게 표시된다. 그렇게 때문에 조회하기 전에         

entityManager.clear(); // 영속성 컨텍스트의 cache값을 지워주고 보자.

```

  • 위 코드를 추가해주면 homeAddress=null, companyAddress=null) 값으로 통일되서 나온다.

  • Embeddable 클래스를 보면 Embeddable에 사용하기 좋은 예시들이 나와있다. 참고해보자.

'Dev > JPA' 카테고리의 다른 글

JPA Converter  (0) 2022.03.02
JPA Native Query  (0) 2022.03.02
JPA @Query  (0) 2022.03.02
JPA OrphanRemoval(+@Where)  (0) 2022.03.02
JPA에서 Transaction 활용하기  (0) 2022.03.02