[JPA 기본] 값 타입 (임베디드 타입), 불변 객체

강신현·2022년 7월 20일
0
post-thumbnail

✅ @Embeddable ✅ @Embedded ✅ @AttributeOverride ✅ a.equals(b)


💡 엔티티 타입

@Entity로 정의하는 객체

  • 식별자 O : 데이터가 변해도 식별자로 지속해서 추적 가능

💡 값 타입

int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체

  • 식별자 X : 값만 있으므로 변경시 추적 불가

1. 기본값 타입

1) 자바 기본 타입(int, double)
2) 래퍼 클래스(Integer, Long)
3) String

  • 생명주기를 엔티티의 의존한다.
    • 즉, 회원을 삭제하면 이름, 나이도 함께 삭제된다.
  • 값 타입은 공유하면 안된다.
    • 왜냐하면, 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안되기 때문이다.
    • int, double (기본 타입) : 절대 공유 X
    • Integer (래퍼 클래스), String (특수한 클래스) : 공유 O, 변경 X

👉 따라서 우리는 java에서 side effect를 고려하지 않고 편하게 사용할 수 있었던 것!

2. 임베디드 타입

(embedded type, 복합 값 타입)

1) 컬렉션 값 타입 (collection value type)

새로운 값 타입을 직접 정의할 수 있음

  • JPA : 임베디드 타입(embedded type)

- 예시

다음에서 비슷한 것끼리 모아 새로운 값 타입을 정의해보자.

  • id
  • name
  • workPeriod : startDate, endDate
  • homeAddress : city, street, zipcode

- 사용법

@Embeddable: 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
(기본 생성자 필수)

- 장점

  • 재사용이 가능하다.
  • 높은 응집도
  • 해당 값 타입만 사용하는 메소드를 만들 수 있음 (ex : Period.isWork())
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티 티에 생명주기를 의존한다.

- 임베디드 타입과 테이블 매핑

임베디드 타입을 사용하기 전과 후에 db상의 매핑하는 테이블은 같다.
하지만 객체는 데이터 뿐만 아니라 메서드(기능)까지 가지고 있기 때문에
임베디드 타입으로 묶어주면 재사용등의 장점들이 생기는 것!

- 코드

  • Member (이전)
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    // 기간 preiod
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    // 주소
    private String city;
    private String street;
    private String zipcode;
}

  • Member (이후)
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    // 기간 preiod
    @Embedded
    private Period workPeriod;

    // 주소
    @Embedded
    private Address homeAddress;
}
  • Address
@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
}
  • Period
@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

- @AttributeOverride (속성 재정의)

한 엔티티에서 같은 값 타입(ex : workAddress, homeAddress)을 사용하면
컬럼 명이 중복되므로 에러가 난다.

이때, 여러개 일 때 @AttributeOverrides, 하나 일 때 @AttributeOverride를 사용해서 컬러 명 속성을 재정의 해줘야 한다.

  • Member
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    // 기간 preiod
    @Embedded
    private Period workPeriod;

    // 주소
    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE"))
    })
    private Address workAddress;
}
  • 결과
create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        WORK_CITY varchar(255), // 1
        WORK_STREET varchar(255), // 2
        WORK_ZIPCODE varchar(255), // 3
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )

3. 값 타입 공유 문제

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용(side effect)가 발생한다.
따라서 값을 복사해서 사용해야 한다.

  1. Main (여러 엔티티에서 값 타입 공유)
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);

member1.getHomeAddress().setCity("newCity");

👉 member1의 city 뿐만아니라 member2의 city도 "newCity" 변경되는 문제가 발생함

  1. Main (값 타입을 복사하여 사용)
Address address = new Address("city", "street", "10000");

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);

Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);

member1.getHomeAddress().setCity("newCity");

👉 member1의 city만 변경됨


위처럼 항상 값을 복사하여 사용하면 공유 참조 문제를 해결할 수 있지만,
객체 타입(임베디드 타입 등)은 기본 타입(int 등)과 다르게 값참조를 전달하기 때문에
특정 팀원이 값을 복사해서 사용하지 않고 기본 타입처럼 사용한다면 문제가 생긴다.

// 값을 복사하여 사용 -> member2 값만 변경 -> 문제 없음
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
// 기본 타입처럼 사용 -> member1, member2 값 모두 변경 -> 문제 발생
Address copyAddress = address;

따라서 객체 타입을 수정할 수 없게 불변 객체를 만들어 부작용을 차단할 수 있다.

💡 불변 객체

생성 시점 이후 절대 값을 변경할 수 없는 객체
생성자로만 값을 설정하고, 수정자(Setter)은 만들지 않으면 된다.

참고로 Integer, String은 자바가 제공하는 대표적인 불변 객체

4. 값 타입 변경

Setter가 없으니 특정 값만 수정하는 것이 불가능해지므로 객체 자체를 다시 새로 만들어 넣어줘야 한다.

member.getAddress.setCity("") : X
member.setAddress(new Address("","","")) : O

5. 값 타입 비교

값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 한다.

- 동일성(identity) 비교

인스턴스의 참조 값을 비교, == 사용

- 동등성(equivalence) 비교

인스턴스의 을 비교, equals() 사용


trouble shooting

1. 에러 코드가 안나옴

e.printStackTrace();

추가하여 에러 발생시 에러코드 출력하게 함

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {


            tx.commit();
        } catch (Exception e){
            e.printStackTrace();
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

2. 기본생성자 error

no default constructor for entity: : hellojpa.address

address에 기본 생성자를 추가하여 해결

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    public Address() {

    }
}
profile
땅콩의 모험 (server)

0개의 댓글