[Spring] JPA 에서 Embedded 타입 활용

WOOK JONG KIM·2022년 11월 23일
1

패캠_java&Spring

목록 보기
66/103
post-thumbnail

앞선 페이지에서 컨버터로 디비 레코드를 어떻게 자바 객체로 변환하여 매핑할지를 다루었음

사실 디비 레코드를 직접 엔티티 필드로 받아서 int,String 과 같은 로우한 타입으로 매핑을 한 뒤에 서비스 로직에서 해당 값들을 변환하거나 해도 문제는 없다

왜 컨버터를 이용해서 반환한 값을 엔티티로 매핑하였을까?
-> 코드의 가독성 측면이 가장 큼

현업에서 임베디드 타입으로 가장 많이 사용하는 필드
->
주문시의 가격 필드(공급가 + 부가세 등의 Set 형태, 여러 도메인에서 사용하기에 임베디드 적용에 적합)

주소 정보 (도, 시, 군, 구, 상세주소)
: 각각 칼럼을 나눠서 처리하는 경우 많음

@Embeddable, @Embedded 사용 예시

사용안할시

User.java

	private String city;
    
    private String district;
    
    private String detail;
    
    private String zipCode;    

DRY 법칙에 위배 됨
-> 객체화 해서 사용하는 것이 바람직

사용 할 시

User.java

public class User extends BaseEntity{
		...
		@Embedded // @Embeddable X
		private Address address;
}

Address.java

@Embeddable // 임베드를 할 수 있는 클래스라고 선언, 이를 통해 엔티티 내부에 선언 가능
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    // 여기서 시, 구는 Enum 으로 사용하는 것도 바람직
    private String city; // 시
    
    private String district; // 구
    
    @Column(name = "address_detail")
    private String detail; // 상세 주소
    
    private String zipCode; // 우편 번호
}

Test 코드

	@Test
    void embedTest(){
        User user = new User();
        user.setName("steve");
        // Embedded 도 일종의 자바 객체기에 new로 생성
        user.setAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06242"));

        userRepository.save(user);

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

test 결과 쿼리

Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        created_at datetime(6) default now(6) comment '생성시간' not null,
        updated_at datetime(6) default now(6) comment '수정시간' not null,
        city varchar(255),
        address_detail varchar(255),
        district varchar(255),
        zip_code varchar(255),
        email varchar(255),
        gender integer,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
    
    // address 라는 클래스로 묶었지만 city, detail, district 각각 칼럼이 생성됨

이대로 진행 시 예상한 결과값이 나오지만 userHistory도 insert문이 실행 되는 것을 확인

UserHistory.java에 @Embedded로 Address 변수 선언 후

EntityListener 수정

	@PostPersist
    @PostUpdate
    public void prePersistAndUpdate(Object o){
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);

        User user = (User) o;
        UserHistory userHistory = new UserHistory();
        userHistory.setUserId(user.getId());
        userHistory.setName(user.getName());
        userHistory.setEmail(user.getEmail());
        userHistory.setGender(user.getGender());
        userHistory.setAddress(user.getAddress());
        userHistory.setUser(user);

        userHistoryRepository.save(userHistory);
    }

배송지 주소 지정 시 생각해보면, 집, 자택 주소 등 여러 주소를 저장하게 됨

Address 라는 클래스 존재 안한다고 생각해보자

User.java

private String homeCity;
private String homeDistrict;
private String homeDetail;
private String homeZipCode;

private String CompanyCity;
private String CompanyDistrict;
private String CompanyDetail;
private String CompanyZipCode;

// 이를 User History에도 변수 그대로 추가해야 할것

이러한 상황을 막기 위해 Embedded를 재활용 해보자

User.java

	@Embedded
    private Address homeAddress;
    
    @Embedded
    private Address companyAddress;
    
    // 히스토리에도 해당 변수 추가

test 코드 및 결과

user.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06242"));
user.setCompanyAddress(new Address("서울시", "성동구", "성수이로 113 제강빌딩", "04794"));
        
//에러 발생
        
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity

내부 컬럼이 두번씩 선언된것 -> 디비에서 한 테이블 컬럼명은 중복 허용 X
-> 이럴때 사용하였는것이 @AttributeOverride


@AttributeOverrides, @AttributeOverride를 통해 Address.java객체의 컬럼 재정의

재정의 하지 않으면 User테이블에 같은 컬럼이 중복되어 오류 발생

@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_detail")),
    @AttributeOverride(name = "zipCode", column = @Column(name = "home_zipCode"))
})
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_detail")),
    @AttributeOverride(name = "zipCode", column = @Column(name = "company_zipCode"))
})
private Address companyAddress;

//history에도 그대로 반영

뒤에 column 속성을 보자면 우리가 선언한 Adddress의 city라는 속성을 home_city라는 이름으로 칼럼을 매핑해주겠다는 의미!

아까 address_detail이라는 컬럼 네임 또한 위에서 선언한 대로 재정의가 됨

@AttributeOverride는 객체를 재활용 하여 좋긴 하지만, 이 또한 너무 길어지니 객체를 여러개 만드는 것이 나을지, @AttributeOverride를 선언하는 것이 좋을 지 생각하고 반영하자

UserEntityListener.java

public class UserEntityListener {
		...
		public void postPersistAndPostUpdate(Object o){
				...
				userHistory.setUser(user);
				userHistory.setHomeAddress(user.getHomeAddress());
		    	userHistory.setCompanyAddress(user.getCompanyAddress());
		
				userHistoryRepository.save(userHistory);
		}
}

테스트 코드 및 결과

//UserRepositoryTest.java
@Test
void embedTest(){		
		User user = new User();
    user.setName("steve");
    user.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06242"));
    user.setCompanyAddress(new Address("서울시", "성동구", "성수이로 113 제강빌딩", "04794"));

    userRepository.save(user);

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

//결과
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        created_at datetime(6) default now(6) comment '생성시간' not null,
        updated_at datetime(6) default now(6) comment '수정시간' not null,
        company_city varchar(255),
        company_detail varchar(255),
        company_district varchar(255),
        company_zip_code varchar(255),
        email varchar(255),
        gender integer,
        home_city varchar(255),
        home_detail varchar(255),
        home_district varchar(255),
        home_zip_code varchar(255),
        name varchar(255),
        primary key (id)
    ) engine=InnoDB

Hibernate: 
    insert 
    into
        user
        (created_at, updated_at, company_city, company_detail, company_district, company_zip_code, email, gender, home_city, home_detail, home_district, home_zip_code, name) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        user_history
        (created_at, updated_at, company_city, company_detail, company_district, company_zip_code, email, gender, home_city, home_detail, home_district, home_zip_code, name, user_id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

User(super=BaseEntity(createdAt=2022-11-23T18:12:18, updatedAt=2022-11-23T18:12:18), id=1, name=martin, email=martin@fastcampus.com, gender=null, homeAddress=null, companyAddress=null)
User(super=BaseEntity(createdAt=2022-11-23T18:12:18, updatedAt=2022-11-23T18:12:18), id=2, name=dennis, email=dennis@fastcampus.com, gender=null, homeAddress=null, companyAddress=null)
User(super=BaseEntity(createdAt=2022-11-23T18:12:18, updatedAt=2022-11-23T18:12:18), id=3, name=sophia, email=sophia@slowcampus.com, gender=null, homeAddress=null, companyAddress=null)
User(super=BaseEntity(createdAt=2022-11-23T18:12:18, updatedAt=2022-11-23T18:12:18), id=4, name=james, email=james@slowcampus.com, gender=null, homeAddress=null, companyAddress=null)
User(super=BaseEntity(createdAt=2022-11-23T18:12:18, updatedAt=2022-11-23T18:12:18), id=5, name=martin, email=martin@another.com, gender=null, homeAddress=null, companyAddress=null)
User(super=BaseEntity(createdAt=2022-11-23T18:12:19.132034, updatedAt=2022-11-23T18:12:19.132034), id=6, name=steve, email=null, gender=null, homeAddress=Address(city=서울시, district=강남구, detail=강남대로 364 미왕빌딩, zipCode=06242), companyAddress=Address(city=서울시, district=성동구, detail=성수이로 113 제강빌딩, zipCode=04794))

임베디드 타입의 null을 넣을 경우

null, 빈객체로 넣는 경우 nullPointerException이 발생하는 것이 아닌 임베디드 된 모든 컬럼의 값은 null로 저장

UserRepository.java

실제 데이터베이스의 결과 확인을 위해 findAllRowRecords 생성
-> 그냥 FindAll() 즉 일반적인 JPA 쿼리 만으로는 확인 불가
-> FindAll() 사용 시에는 null,빈 객체 차이가 존재함

@Query(value = "select * from user", nativeQuery = true)
List<Map<String, Object>> findAllRowRecords();

테스트 코드

	@Test
    void embedTest(){
        User user = new User();
        user.setName("steve");
        // Embedded 도 일종의 자바 객체기에 new로 생성
        user.setHomeAddress(new Address("서울시", "강남구", "강남대로 364 미왕빌딩", "06242"));
        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());

        // 위 두 User를 통해 Address 라는 객체가 null 일때와 Address는 존재하지만 내부
        // 칼럼이 null일 때를 비교해보자

        userRepository.save(user2);

        userRepository.findAll().forEach(System.out::println);
        userRepository.findAllRowRecords().forEach(a -> System.out.println(a.values()));
    }

실행 결과

JPA에서 객체와 null로 넣을때 결과 차이가 발생하는 이유는 영속성 컨텍스트 캐쉬
-> EntityManager clear()를 실행하면 DB에서 조회하기 때문에 둘 다 null로 표시(id 7, id 8)

//JPA를 이용한 조회 (userRepository.findAll()
id=6, name=steve, email=null, gender=null, homeAddress=Address(city=서울시, district=강남구, detail=강남대로 364 미왕빌딩, zipCode=06242), companyAddress=Address(city=서울시, district=성동구, detail=성수이로 113 제강빌딩, zipCode=04794))
id=7, name=joshua, email=null, gender=null, homeAddress=null, companyAddress=null)
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))

//데이터베이스 조회 (userRepository.findAllRowRecords())
[6, 2022-11-23 18:24:54.587017, 2022-11-23 18:24:54.587017, 서울시, 성수이로 113 제강빌딩, 성동구, 04794, null, null, 서울시, 강남대로 364 미왕빌딩, 강남구, 06242, steve]
[7, 2022-11-23 18:24:54.618505, 2022-11-23 18:24:54.618505, null, null, null, null, null, null, null, null, null, null, joshua]
[8, 2022-11-23 18:24:54.621077, 2022-11-23 18:24:54.621077, null, null, null, null, null, null, null, null, null, null, jordan]

Embeddable java docs

Specifies a class whose instances are stored as an intrinsic part of an owning entity and share the identity of the entity.

Each of the persistent properties or fields of the embedded object is mapped to the database table for the entity.

Note that the Transient annotation may be used to designate the non-persistent state of an embeddable class.

 Example 1:

 @Embeddable public class EmploymentPeriod { 
    @Temporal(DATE) java.util.Date startDate;
    @Temporal(DATE) java.util.Date endDate;
   ... 
 }

 Example 2:

 @Embeddable public class PhoneNumber {
     protected String areaCode;
     protected String localNumber;
     @ManyToOne PhoneServiceProvider provider;
     ...
  }

 @Entity public class PhoneServiceProvider {
     @Id protected String name;
      ...
  }

 Example 3:

 @Embeddable public class Address {
    protected String street;
    protected String city;
    protected String state;
    @Embedded protected Zipcode zipcode;
 }

 @Embeddable public class Zipcode {
    protected String zip;
    protected String plusFour;
  }
profile
Journey for Backend Developer

0개의 댓글