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

songh·2024년 11월 22일

Spring

목록 보기
49/51

JPA의 데이터 타입 분류

(1) 엔티티 타입

@Entity 어노테이션을 붙이는 객체로, 데이터가 변해도 id값처럼 식별자로 지속해서 추적이 가능하다. 예를들어, 회원의 키나 나이가 변경해도 색별자인 id=100은 변하지 않는 것처럼 id값으로 지속적인 객체 값 확인이 가능하다.

(2) 값 타입

int, Integer, String, 임베디드 타입, 값 타입 컬렉션이 여기에 속한다. 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 식별자가 없고 값만 있기 때문에 변경되었을때 추적이 어렵다. 숫자 100을 200으로 단순히 값을 변경하면 이후에 추적이 어려울 수 밖에 없다.

값 타입 분류

🔎 기본값 타입 : 자바 기본 타입(int, double, long 등), 래퍼 클래스(Integer, Long 등), String 이 해당된다.

🔎 임베디드 타입 : 기본 값 타입이 여러 개 묶여진 것을 말한다.

🔎 컬렉션 값 타입 : List<String>이나 List<Address(임베디드 타입)> 처럼 기본 값 타입이나 임베디드 타입을 갖는 컬렉션을 말한다. 쉽게 말하면 값타입으로 된 컬렉션이다.

값 타입 - [1] 기본 값 타입

기본 값 타입은 자신의 생명주기를 소속된 엔티티에 의존하게 된다. 엔티티가 삭제되면 그 안에 세팅된 기본값 타입의 값들도 모두 삭제되고 저장하면 같이 저장되는 것처럼 한 몸이 되기 때문이다. 값 타입은 값이 공유될 수 있다는 문제가 있지만 기본값 타입은 항상 값을 복사하기에 참조값이 공유되지 않아서 문제는 없다. 또 Integer같은 래퍼 클래스나 String 클래스로 된 객체는 공유 가능하지만 변경할 수 없는 불변한 값이어서 사용하는 이유이기도 하다.

값 타입 - [2] 임베디드 값 타입

기본 값타입 여러 개로 새롭게 값 타입을 정의할 수 있는 타입이다. int, String 같은 값 타입이 모아서 만들어진다. 임베디드 타입을 만들면 객체를 설명할때 더 추상화해서 쉽게 설명할 수 있다.



Period와 Address로 임베디드 타입을 만들고 Member에서 사용하는 것으로 @Embeddable@Embedded로 표시한다. 임베디드 타입은 기본 생성자가 필수다.

@Embeddable // 값 타입 정의
public class Address {
    // Address
    String city;
    String street;
    String zipcode;
//    private Parent parent; // 엔티티 가질 수 있음
	...
}
@Embeddable // 값 타입 정의
public class Period {
    // Period
    LocalDateTime startDate;
    LocalDateTime endDate;
    ...
 }
@Entity
public class MemberTest {
    @Id @GeneratedValue
    private Long id;
    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;
    ...
}

여러 엔티티에서 중복해서 임베디드 타입을 사용할 수 있어 재사용성이 높으며 높은 응집도를 가질 수 있다. 임베디드 타입을 포함해서 모든 값 타입은 값 타입을 소유한 엔티티에게 생명주기를 의존하며, 같은 생명주기를 가지게 된다.

임베디드 타입은 엔티티 안에 있는 값일 뿐, 매핑하는 테이블에 해당 내용이 기입되는 것일 뿐이다. 임베디드 타입 사용 전과 후의 매핑 테이블은 변화되는 것이 없다. 잘 설계한 ORM 어플리케이션은 매핑한 테이블 수보다 클래스 수가 더 많다. 임베디드 타입은 엔티티도 가질 수 있고 임베디드 타입을 또 가질 수 있다. @AttributeOverrides@AttributeOverride를 사용해서 같은 임베디드 타입 속성명을 바꿀수도 있다.

값 타입과 불변 객체

임베디드 타입을 여러 엔티티에서 공유하면 위험하고, 임베디드 타입은 다시 말해 객체로 사용되기 때문에 복사해도 원본 객체에 대한 참조값이 복사되는 것이다. 따라서 아무리 참조값을 반환받지 않으려 해도 개발자 실수로 인해 돌려쓰는 일이 있을 수 있다. 자바 기본값 타입은 값을 복사할 수 있지만, 임베디드 타입은 객체라서 = 대입연산을 써도 원복 객체의 참조값을 반환받는다. 따라서 객체가 공유되는 것을 막을 수도 없고 완전히 피하기도 힘들다.

 Address address = new Address("city", "street", "zipcode");

MemberTest member = new MemberTest();
member.setName("member1");
member.setAddress(address);
em.persist(member);

Address copyAddress = new Address(address.getCity(), address.getStreet(),
address.getZipcode()); // [2-1의 대안]값 타입은 복사해서 사용
MemberTest member2 = new MemberTest();
member2.setName("member2");
//            member2.setAddress(address); // [1]값 타입 공유
member2.setAddress(copyAddress);
em.persist(member2);
member.getHomeAddress().setCity("new City"); //[1] 값 타입 공유 : UPDATE 2회, [2] UPDATE 1회, 공유X

[1]처럼 임베디드 타입 Address의 객체 address를 member와 member2 모두 같은 곳을 가리키게 된다. member의 address를 setter로 값 변경해도, member와 member2 모두 UPDATE 되는 것을 볼 수 있다. [1]의 대안으로 [2]는 getter로 기본값 타입의 값만 받아오고 copyAddress라는 완전히 새로운 객체로 정의해서 setter로 값을 완전히 대체해주는 방법이다. 따라서 임베디드 타입을 사용할땐, 참조가 공유되지 않도록 1. Address에서 setter를 만들지 않거나 2. 생성자를 private으로 막아두면 부작용을 미리 막을 수 있다.

값 타입의 비교

값 타입은 인스턴스가 달라도 그 안에 인스턴스들의 값이 같으면 같은 것으로 봐야한다. 기본값 타입은 == 타입비교가 아닌, a.equals(b)라는 동등성 비교를 해야한다. * 동등성비교 : 인스턴스의 값을 비교하는 것(address1과 address2라는 임베디드 객체의 속성값이 모두 같다면 동등성 비교로 같다고 해야한다.) equals메서드는 적절하게 재정의해서 사용해야 한다. 이유는 equals메서드의 기본 내용이 == 비교이기 때문이다.

int a = 10;
int b = 10;

System.out.println("a == b "+ (a== b)); // true

Address address1 = new Address("city", "street", "zipcode");
Address address2 = new Address("city", "street", "zipcode");

System.out.println("address1 == address2 " + (address1 == address2)); // false
System.out.println("address1 == address2 " + (address1.equals(address2))); // false, equals 기본이 == 비교(재정의 후 true)
@Embeddable // 값 타입 정의
public class Address {
    // Address
    String city;
    String street;
    String zipcode;
//    private Parent parent; // 엔티티 가질 수 있음

    //Setter x / private => 객체 참조 공유 방지, 불변으로 생성
    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public String getZipcode() {
        return 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);
    }
    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}

값 타입 컬렉션(권장x)

값 타입을 컬렉션에 담아서 쓰는 것을 "값 타입 컬렉션"이라고 말한다. 예를 들면 List<String>이나 Set<Address>가 있을 것이다. 이 값 타입 컬렉션을 엔티티가 가지게 되면 DB에서는 각 컬렉션에 대해 각 DB 테이블로 만들고 값 타입 속성들을 모두 PK로 만들어 일대다 관계로 매핑된다.

@Entity
public class MemberTest {
    @Id @GeneratedValue
    private Long id;
    private String userName;
    @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<>();
	...
}

(1) 값 타입 컬렉션 - 저장(문제없음)

 MemberTest member = new MemberTest();
 member.setUserName("member1");
 member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

// 값 타입 컬렉션
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

// 값 타입 컬렉션
member.getAddressHistory().add(new Address("old1", "street", "zipcode"));
member.getAddressHistory().add(new Address("old2", "street", "zipcode"));
em.persist(member); // persist 1회로 관련 값 타입 갯수대로 각각 INSERT, 라이프사이클이 member에 종속(모두 값 타입)

member.persist()할때 favoriteFood 컬렉션 저장요소 3개와 addressHistory 컬렉션 저장요소 2개에 대한 persist()가 모두 일어나서 INSERT 6개가 발생한다. 라이프 사이클이 엔티티 member에 종속되기 때문이다.

(2) 값 타입 컬렉션 - 조회(문제없음)

MemberTest member = new MemberTest();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

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

member.getAddressHistory().add(new Address("old1", "street", "zipcode"));
member.getAddressHistory().add(new Address("old2", "street", "zipcode"));

em.persist(member); // persist 1회로 관련 값 타입 갯수대로 각각 INSERT, 라이프사이클이 member에 종속(모두 값 타입)
            
em.flush();
em.clear();
System.out.println("===구분선===");
MemberTest findMember = em.find(MemberTest.class, member.getId()); // SELECT MEMBER, 지연로딩
System.out.println("===구분선===");



//컬렉션은 지연로딩
List<Address> addressHistory = findMember.getAddressHistory(); // MEMBER_ID 외래키로 SELECT 1회
for (Address address : addressHistory) {
	System.out.println("address = "+ address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
	System.out.println("food = " + favoriteFood); // SELECT 1회
}

값 타입 컬렉션은 지연로딩으로 기본적으로 설정되어있어서 em.find(member)할때 SELECT 쿼리가 MEMBER만 발생한다. 이후 컬렉션이 사용될 일이 있을때 MEMBER_ID로 WHERE절을 통해 각각 ADDRESSHISTORY, FAVORITEFOOD를 SELECT하므로 문제없다.

(3) 값 타입 컬렉션 - 삭제 (문제있음🚨🚨🚨🚨)

MemberTest member = new MemberTest();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

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

member.getAddressHistory().add(new Address("old1", "street", "zipcode"));
member.getAddressHistory().add(new Address("old2", "street", "zipcode"));

em.persist(member); // persist 1회로 관련 값 타입 갯수대로 각각 INSERT, 라이프사이클이 member에 종속(모두 값 타입)

em.flush();
em.clear();
System.out.println("===구분선===");
MemberTest findMember = em.find(MemberTest.class, member.getId()); // SELECT MEMBER, 지연로딩
System.out.println("===구분선===");




//homecity->newcity
//            findMember.getHomeAddress().setCity("newCity"); 객체 참조 공유되므로 올바르지x
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); // 완전 교체

// 치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");//DELETE FAVORITE_FOOD
findMember.getFavoriteFoods().add("한식"); // List<String> String 자체가 값 타입, 완전 교체, INSERT FAVORITE_FOOD

findMember.getAddressHistory().remove(new Address("old1", "street", "zipcode")); //equals 재정의 필수, DELETECT ADDRESS WHERE MEMBER_ID
findMember.getAddressHistory().add(new Address("newCity1", "street", "zipcode")); // INSERT ADDRESS 2회

addressHistory 중 Address 1개를 변경하고 싶어서 new Address("old1", "street", "zipcode")이랑 똑같은 Address를 삭제하고 새로운 Address를 삽입했지만, MEMBER_ID=member.getId()인 모든 데이터를 DELETE하고 다시 member_id인 모든 내용을 INSERT했다. 왜 이렇게 했을까? 값 타입 컬렉션에서 변경사항이 있을때, 주인 엔티티와 연관된 모든 데이터는 삭제하고 값 타입 컬렉션에 있는 값들을 모두 다시 저장하기 때문이다.

값 타입 컬렉션 대안 = 일대다 단방향 매핑 만들기 + 값 타입(임베디드 타입)을 엔티티로 생성

(1) 저장

정말 단순한건 값 타입 컬렉션으로 만들고 그 외에는 모두 엔티티로 만들어서 일대다 단방향으로 만드는 것이 속편하다. 그리고 영속성전이(cascade)와 고아객체 제거(orphanRemoval=true)로 설정해서 완전히 일쪽 엔티티에 라이프 사이클이 종속되도록 만든다. * 일대다 단방향은 일에서 list.add로 저장하면 다쪽에 update쿼리가 나감에 주의

@Entity(name = "Address")
public class AddressEntity {
    @Id @GeneratedValue
    private Long id; // PK, 값 타입 컬렉션 대신 엔티티(일대다 단방향 매핑)
    private Address address;
	...
}
@Entity
public class MemberTest {
    @Id @GeneratedValue
    private Long id;
    private String userName;
    @Embedded
    private Address homeAddress;

    @ElementCollection// 값 타입 컬렉션(일대다로 별도 테이블 생성)
    @CollectionTable(name = "FAVORITE_FOOD",// 매핑 테이블명
        joinColumns = @JoinColumn(name = "MEMBER_ID") // 외래키
    )
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    // 값 타입 컬렉션 대안-엔티티 매핑(일대다 단방향)
    @OneToMany(cascade = ALL, orphanRemoval = true) // 권장
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
    ...
}
MemberTest member = new MemberTest();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

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

member.getAddressHistory().add(new AddressEntity("old1", "street", "zipcode")); //ADDRESS 외래키 UPDATE(일대다 단방향)
member.getAddressHistory().add(new AddressEntity("old2", "street", "zipcode")); //ADDRESS 외래키 UPDATE(일대다 단방향)
//값 타입을 엔티티로 승급, 수정가능(권장)

em.persist(member); // persist 1회로 관련 값 타입 갯수대로 각각 INSERT, 라이프사이클이 member에 종속(모두 값 타입)

(2) 수정 - 엔티티랑 똑같이 사용(헷갈려서 작성)

MemberTest member = new MemberTest();
member.setUserName("member1");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));

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

AddressEntity ade1 = new AddressEntity("old1", "street", "zipcode");
AddressEntity ade2 = new AddressEntity("old2", "street", "zipcode");
member.getAddressHistory().add(ade1); //ADDRESS 외래키 UPDATE(일대다 단방향)
member.getAddressHistory().add(ade2); //ADDRESS 외래키 UPDATE(일대다 단방향)
//값 타입을 엔티티로 승급, 수정가능(권장)

em.persist(member); // persist 1회로 관련 값 타입 갯수대로 각각 INSERT, 라이프사이클이 member에 종속(모두 값 타입)

em.flush();
em.clear();
System.out.println("===구분선===");
MemberTest findMember = em.find(MemberTest.class, member.getId()); // SELECT MEMBER, 지연로딩
AddressEntity changeAddress = findMember.getAddressHistory().get(0);
changeAddress.setCity("newnewnew");

0개의 댓글