[자바 ORM 표준 JPA 프로그래밍 - 기본편] 09. 값 타입

Turtle·2024년 6월 21일
0
post-thumbnail

🙄JPA의 데이터 타입 분류

  • ✔️엔티티 타입
    • @Entity로 정의한 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
  • ✔️값 타입
    • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 기본값 타입 / 임베디드 타입 / 컬렉션 값 타입

🙄임베디드 타입

  • ✔️임베디드 타입 사용법
    • @Embeddable : 값 타입을 정의하는 곳에 표시
    • @Embedded : 값 타입을 사용하는 곳에 표시
    • 기본 생성자 필수
@Entity
@Table(name = "MEMBERS")
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

	////////////////////////////////// 임베디드 타입
    @Embedded
    private Period period;

    @Embedded
    private Address address;
	//////////////////////////////////

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    public Team getTeam() {
        return team;
    }
}

  • ✔️임베디드 타입과 테이블 매핑
    • 임베디드 타입은 엔티티의 값일 뿐이다.
    • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
    • 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능
    • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
  • ✔️@AttributeOverride 속성 재정의
    • 한 엔티티에서 같은 값 타입을 사용하면?
    • Ex. 집 주소와 근무지 주소와 같이 주소 타입을 중복해서 사용하면 컬럼명이 중복됨
    • @AttributeOverrides와 @AttributeOverride를 사용해서 컬럼명 속성을 재정의
  • ✔️임베디드 타입과 null
    • 임베디드 타입이 null이라면 매핑한 컬럼 값은 모두 null이 된다.
@Entity
@Table(name = "MEMBERS")
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @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;
    ////////////////////////////////////////////////////

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
}

🙄값 타입과 불변 객체

  • ❗문제
    • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험
    • 사이드 이펙트가 발생
  • ✔️값 타입 복사
    • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.
    • 대신에 값을 복사해서 사용한다.
    • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
    • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
    • 자바 기본 타입에 값을 대입하면 값을 복사한다.
    • 객체 타입의 경우 참조 값을 복사하기 때문에 객체의 공유 참조를 피할 수 없게 된다.
  • ✔️불변 객체👍
    • 객체 타입을 수정할 수 없게 만들기
    • 값 타입은 불변 객체로 설계해야함
    • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없도록
    • 생성자로만 값을 설정하고 수정자를 만들지 않기
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("shop");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
            Address address = new Address("city", "street", "10000");

            Member member1 = new Member();
            member1.setName("member1");
            member1.setHomeAddress(address);
            em.persist(member1);
			//////////////////////////////////////////////// 새로 만들기(불변 객체, 값을 복사하는 것은 기본값의 경우 문제가 없으나 참조값의 경우 문제가 발생(공유 참조))
            Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
			////////////////////////////////////////////////
            
            Member member2 = new Member();
            member2.setName("member2");
            member2.setHomeAddress(newAddress);
            em.persist(member2);

            et.commit();
        } catch (Exception e) {
            et.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

🙄값 타입의 비교

  • ✔️값 타입의 비교
    • 동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
      • 자바에서 기본적으로 == 비교의 경우에는 참조 값을 기준으로 하기 때문에 인스턴스 값이 같다 하더라도 False가 나오게 됨
      • 또한 equals() 메서드의 경우 역시 기본적으로 == 비교이기 때문에 인스턴스의 참조 값을 기준으로 비교를 한다.
    • 동등성 비교 : 인스턴스의 값을 비교, equals() 사용
    • 값 타입은 동등성 비교를 해야 한다.
    • 값 타입의 경우 equals() 메서드를 적절하게 재정의하는 것이 필요하다.

🙄값 타입 컬렉션

  • ✔️값 타입 컬렉션(권장X)
    • 값 타입을 하나 이상 저장할 때 사용한다.
    • @ElementCollection, @CollectionTable 사용
    • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
    • 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
    • 값 타입 컬렉션 대신에 일대다 관계를 사용하도록 권장
    • 이 때, 1(일)이 연관관계의 주인이 되며 UPDATE 쿼리가 호출됨
@Entity
@Table(name = "MEMBERS")
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

	////////////////////////////////////////////////////////////////////
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();
	////////////////////////////////////////////////////////////////////

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

	// Getter/Setter ...
}
  • ✔️값 타입 컬렉션 사용 - 저장 예제
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("shop");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
            Member member = new Member();
            member.setName("member1");
            member.setHomeAddress(new Address("city", "street", "10000"));

			///////////////////////////////////////////////////////////////
            // 컬렉션 add() 메서드
            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("족발");
            member.getFavoriteFoods().add("피자");

            member.getAddressHistory().add(new Address("old1", "street", "10000"));
            member.getAddressHistory().add(new Address("old2", "street", "10000"));
            ///////////////////////////////////////////////////////////////
            em.persist(member);

            et.commit();
        } catch (Exception e) {
            et.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • ✔️값 타입 컬렉션 사용 - 조회 예제
    • 값 타입 컬렉션에 대한 쿼리가 존재하지 않는다.
    • 이는 값 타입 컬렉션도 지연 로딩을 사용한다는 것을 의미한다.
    • 지연 로딩을 사용한다는 것은 결국 컬렉션을 가져와서 컬렉션과 관련된 메서드를 호출해야지 실제 쿼리가 호출이 된다는 것을 말한다.
public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Member member = new Member();
            member.getAddressHistory().add(new Address("서울특별시", "노원구", "공릉동"));
            member.getFavoriteFoods().add("교촌치킨");
            member.getFavoriteFoods().add("지코바");
            em.persist(member);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

실행 결과

Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.name,
        m1_0.TEAM_ID,
        m1_0.endDate,
        m1_0.startDate 
    from
        MEMBERS m1_0 
    where
        m1_0.MEMBER_ID=?
  • ✔️값 타입 컬렉션 사용 - 수정 예제
    • 불변 객체를 활용해서 수정을 해야 한다.
    • 복사해서 바꾸는 것이 아니라 아예 새로 갈아끼워야 한다.
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("shop");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
            Member member = new Member();
            member.setName("member1");
            member.setHomeAddress(new Address("city", "street", "10000"));

            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("족발");
            member.getFavoriteFoods().add("피자");

            member.getAddressHistory().add(new Address("old1", "street", "10000"));
            member.getAddressHistory().add(new Address("old2", "street", "10000"));
            em.persist(member);

            em.flush();
            em.clear();

			///////////////////////////////////////////////////////////////
            Member findMember = em.find(Member.class, member.getId());
            findMember.setHomeAddress(new Address("NewCity", "NewStreet", "20000"));
            // remove() 후 add()
            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");

			// equals() 메서드는 기본적으로 인스턴스의 참조값을 기준으로 비교
            // equals() 메서드를 재정의하지않는다면 이렇게 작성해도 삭제가 안 된다.
            // 따라서 equals() 메서드를 재정의해서 사용해야하며 역시 remove() 후 add() 
            findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
            findMember.getAddressHistory().add(new Address("new1", "street", "10000"));
            ///////////////////////////////////////////////////////////////


            et.commit();
        } catch (Exception e) {
            et.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • ✔️값 타입 컬렉션의 제약사항
    • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
    • 값은 변경하면 추적이 어렵다.
    • 값 타입 컬렉션에 변경사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하게 된다.(효율성 측면 저하)
    • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 키를 구성해야 함
      • null 입력 X, 중복 저장 X
  • ✔️값 타입 컬렉션 대안
    • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
    • 일대다 관계를 위한 엔티티를 만들고 여기서 값 타입을 사용
    • 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용
  • ✔️일대다 관계를 적용한 코드 리팩토링
@Entity
@Table(name = "MEMBERS")
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

	//////////////////////////////////////////////////////////////// 일대다 단방향으로... 일(1)이 연관관계의 주인 → UPDATE 쿼리 호출됨
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
	////////////////////////////////////////////////////////////////

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

	// Getter/Setter ...
}
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;

    @Embedded private Address address;

    public AddressEntity(String city, String street, String number) {
        this.address = new Address(city, street, number);
    }

    public AddressEntity() {

    }

	// Getter/Setter
}

🙄값 타입 매핑 예제

✔️Address 클래스(@Embeddable)

@Embeddable
public class Address {
	private String city;
	private String street;
	private String zipcode;

	// 기본 생성자
	public Address() {
	}

	public Address(String city, String street, String zipcode) {
		this.city = city;
		this.street = street;
		this.zipcode = zipcode;
	}

	// 값 타입 → 인스턴스의 값을 기준으로 비교(equals() 메서드 재정의 필수) 
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		Address address = (Address) o;
		return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
	}

	// 값 타입 → 인스턴스의 값을 기준으로 비교(hashCode() 메서드 재정의 필수) 
	@Override
	public int hashCode() {
		return Objects.hash(city, street, zipcode);
	}
}
@Entity
@Table(name = "MEMBERS")
public class Member {
	@Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address Address;
    
    // ...
} 
@Entity
public class Delivery extends BaseEntity {
    @Id @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;

    @Embedded
    private Address address;
    private DeliveryStatus status;
    
    // ...
}

0개의 댓글