[JPA] 임베디드 타입 (@Embeddable)

dooboocookie·2022년 12월 3일
0
post-thumbnail

목표

  • 해당 프로젝트에서 병원 엔티티에는 여러 필드가 필요하다.
  • 이중에는 병원 이름, 병원 전화번호와 같은 각 값이 의미하는바가 다른 필드가 있는 반면
  • 위도,경도,시/도,...,번지 주소 와 같이 주소에 대한 정보들로 일루어진 필드들도 있다.
    • 이를 임베디드 타입으로 묶어 새로은 값 타입을 정의하여 나타낼 것이다.

값 타입

종류

  • JPA에는 2가지 타입 분류가 있다.
    • Entity 타입
      • @Entity 어노테이션으로 엔티티로 정의한 객체 타입
      • 해당 엔티티 객체의 내부 필드 값을 변경하여도 PK를 통하여 식별 가능
    • 값 타입
      • 자바에서 primitive type이나 wrapper class나 String (int, Integer, double, Double, String, ...)
      • 임베디드 타입 (복합 값 타입)
      • 컬렉션 값 타입 (여러 이유로 사용하지 않을 것임, 1:N 연관관계로 대체)

생명 주기

  • 값 타입은 엔티티의 생명주기에 의존한다.
  • 값 타입은 다른 엔티티와 공유해서는 안된다.
    • A 엔티티에 필드 값을 변경했는데, B 엔티티의 필드 값이 변경되는 것을 막아야한다.
  • 값 타입은 다른 변수에 전달할 때 값을 복사한다.

임베디드 타입

여러 값 타입을 합쳐서 하나의 임베디드 타입으로 새로운 값 타입으로 정의하여 사용

  • 위의 병원 엔티티를 봤을 때 주소영업시간정도를 값 타입으로 만들 수 있을 것이다.

사용 목적

  • 여러 값 타입을 묶어 하나의 임베디드 타입으로 만들면 여러 엔티티에서 재사용이 가능한 값 타입이 된다.
  • 그리고 예를 들어 주소와 관련된 책임과 역할을 엔티티가 아니라 임베디드 타입에 있으므로, 높은 응집도를 갖는다.
    • 주소를 저장할 때, 위도 경도 값을 API를 통하여 가져오는 로직이라던지,
    • 현재 영업중인지 체크하는 메소드라던지,
    • 위와 같은 기능을 해당 임베디드 타입 내부에 넣을 수 있다.

임베디드 타입의 맵핑

  • @Embeddable
    • 임베디드 타입으로 정의하는 class에 놓는 어노테이션
  • Embedded
    • 해당 필드가 임베디드 타입이라 표시하는 어노테이션
  • Address.java
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class Address {

    private String sido; //서울
    private String siqungu; //서대문구
    private String bname; //북가좌동
    private String jibunAddress; //서울 서대문구 북가좌동 328-51
    private String roadAddress; //서울 서대문구 증가로29길 20-14
    private String sangse;
    private double x;
    private double y;
    private Long bCode;
}
  • Hospital.java
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Hospital extends TimeStamped {

    //1. PK / 2. 자동으로 값 생성(아이덴티티 전략 -> 디비가 생성)
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long hosId;
    private String hosName;
    private String hosPhone;
    @Embedded
    private Address hosAddress;
    // ...
}

  • 클래스 즉,엔티티임베디드 타입 클래스 간 다이어그램은 다음과 같다.
  • 하지만 생성되는 Hospital 테이블의 모든 컬럼이 다 생성된다.
    • 임베디드 타입으로 따로 묶기 전과 동일하다.

"잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음"
김영한 - 자바 ORM 표준 JPA 프로그래밍 - 기본편

등록

  • 임베디드 타입은 객체로서 등록해주면 된다.
  • 임베디드 타입이 null이면 안에 있는 컬럼 값 모두가 null이다.
  • Service.java
Address address = Address.createAddress("지번주소", "상세주소");

Hospital hospital = Hospital.createHospital(hospitalSaveForm,address);

em.persist(hodpital);
  • Hosptal.java
public static Hospital createHospital(HospitalSaveForm form, Address hosAddress) {
    return Hospital.builder()
			// ...
            .hosAddress(hosAddress)
            // ...
            .build();
 }

임베디드 타입은 절대 공유하면 안된다.

  • 해당 객체를 값을 다른 엔티티에도 부여하고 싶으면 복제해서 사용해야한다.

불변 객체(immutable object)

  • 위와 같은 이유들로 임베디드 타입은 불변 객체로 만들어야한다.
  • 불변 객체는 생성자를 통해 1번 만들어지면 다시 값을 바꿀 수 없게 해야한다.
  • 방법
    • setter를 정의하지 않거나, private으로 둔다.

수정

  • 임베디드 타입 내에 어떤 값을 변경하고 싶을 때는 객체를 새로 만들어서 객체를 갈아 끼워야한다.
  • Service.java
Hospital hospital = em.find(Hospital.class, 1L);

Address address = Address.createAddress("변경된 지번", "변경된 상세");

hostpital.setAddress(address); // 실제로는 세터가 아닌 update하는 메소드를 사용

삭제

  • 임베디드 타입의 생명주기는 엔티티에 의존하므로 엔티티가 삭제되면 같이 삭제된다.

실제 코드

  • Hospital.java
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Hospital extends TimeStamped {

    //1. PK / 2. 자동으로 값 생성(아이덴티티 전략 -> 디비가 생성)
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long hosId;

    private String hosName;

    private String hosPhone;

    @Enumerated(EnumType.STRING)
    private HosStatus hosStatus;

    @Enumerated(EnumType.STRING)
    private HosBooking hosBooking; // 예약 가능 상태를 나타내는 값

    @Embedded
    private Address hosAddress;

    private String hosOpenhour;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "hos_sigudong")
    private Sigudong hosSigudong;

    @OneToMany(mappedBy = "hospital",orphanRemoval = true)
    private List<HosImg> hosImgs = new ArrayList<>();

    /*== 빌더패턴을 통한 생성 메소드 ==*/
    public static Hospital createHospital(HospitalSaveForm form, Address hosAddress, Sigudong dong) {
        return Hospital.builder()
                .hosName(form.getHosName())
                .hosPhone(form.getHosPhone())
                .hosStatus(HosStatus.OPEN)
                .hosBooking(HosBooking.POSSIBLE)
                .hosAddress(hosAddress)
                .hosSigudong(dong)
                .hosOpenhour(form.getHosOpenHour())
                .build();
    }

     /*상태 유지를 이용한 업데이트*/
    public void update(HospitalUpdateForm hospitalUpdateForm, Address address, Sigudong sigudong) {

        this.hosName = hospitalUpdateForm.getHosName();
        this.hosPhone = hospitalUpdateForm.getHosPhone();
        this.hosOpenhour = hospitalUpdateForm.getHosOpenHour();
        this.hosAddress = address;
        this.hosSigudong = sigudong;
    }
}
  • Address.java
@Embeddable
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Address {

    private String sido; //서울
    private String siqungu; //서대문구
    private String bname; //북가좌동
    private String jibunAddress; //서울 서대문구 북가좌동 328-51
    private String roadAddress; //서울 서대문구 증가로29길 20-14
    private String sangse;
    private double x;
    private double y;
    private Long bCode;

    public static Address createAddress (String jibunAddress, String sangse) {
        AddressToCoordinate addressToCoordinate = new AddressToCoordinate();
        JSONObject jsonObject = addressToCoordinate.coordinate(jibunAddress);
        JSONObject roadJoso = addressToCoordinate.newAddressJson(jsonObject);
        Long bCode = addressToCoordinate.bCode(jsonObject);
        return Address.builder()
                .sido((String) roadJoso.get("region_1depth_name"))
                .siqungu((String) roadJoso.get("region_2depth_name"))
                .bname((String) roadJoso.get("region_3depth_name"))
                .jibunAddress(jibunAddress)
                .roadAddress((String) roadJoso.get("address_name"))
                .sangse(sangse)
                .x(Double.parseDouble((String) roadJoso.get("x")))
                .y(Double.parseDouble((String) roadJoso.get("y")))
                .bCode(bCode)
                .build();
    }
}
profile
1일 1산책 1커밋

0개의 댓글

관련 채용 정보