[JPA] 6.값 타입

재우·2025년 4월 19일

JPA

목록 보기
6/11

값 타입

값타입은 기본값타입, 객체타입, 임베디드타입, 컬렉션값타입이 있다.

  1. 기본값 타입
    • 기본 타입(int, double..)
    • 래퍼 클래스(Integer, Long)
    • String
  2. 객체 타입(=@Entity가 붙지않은 클래스의 타입)
  3. 임베디드 타입
  4. 컬렉션 값 타입


값 타입과 불변 객체

값 타입 공유 참조

  • 객체 타입이나 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유할 수는 있지만 공유하면 위험하다.
  • 공유 참조로 인해 예상치 못한 곳에서 문제가 발생하는 부작용(side effect)이 발생할 수 있다.


다음과 같이 있을 때 OldCity를 NewCity로 변경하면 회원1과 회원2 모두 NewCity로 값이 바뀌게 된다.

Address address = new Address("city", "address", "10000");

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

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

위 코드는 member1이나 member2나 컬럼에 모두 같은 값이 들어간다.

어느날 member1만 컬럼 값을 수정하고 싶어서

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

위 코드를 실행해서 member1의 값만 바뀌는 것을 원했지만, member2도 똑같이 newCity라는 값으로 업데이트 되었다.

이럴 때는 어떻게 해결해야 할까?

값 타입 복사

값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하기 때문에 값(인스턴스)를 복사해서 사용하자.

Address address = new Address("city", "address", "10000");

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

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

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

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

이렇게 실행하면 member1의 값만 바뀌게 된다.

하지만, 실수로 copyAddress를 사용하지 않고 그대로 address를 사용했을 때 컴파일러 단계에서 오류를 띄워줄 수 있는 방법이 있을까?
-> 없다.

객체 타입의 한계

  • 항상 값(인스턴스)을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.

int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4;
// a = 10, b = 4
Address a = new Address("old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("new");
// a.city = new, b.city = new

불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다.

  • 기본 타입(int, double..)을 제외한 값 타입은 불변 객체(immutable object)로 설계해야함

    • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
    • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않거나 private로 만들면 된다.
    • cf) int b = a는 단순히 a의 값(숫자 10)을 복사해서 b에 대입했기 때문에 이후에 b = 4로 바꿔도 a에는 아무런 영향이 없다. 기본 타입은 값을 복사하기 때문에 내부 값이 바뀌더라도 다른곳에 영향을 주지않는다. 그래서 불변 설계가 필요없다.
  • 참고로 Integer, String은 자바가 제공하는 대표적인 불변 객체다.

String str = "hello";
str = str + " world";
  • 이 코드에서 "hello"라는 문자열 객체는 한 번 생성되면 바뀌지 않는다.
  • str + " world"를 하면 기존의 문자열을 수정하는 게 아니라, 새로운 "hello world" 문자열 객체를 생성하고, str이 그걸 참조하게 된다.
  • str이 참조하는 "hello" 문자열이 + 연산에 사용되고, 결과로 "hello world"가 만들어지고, 그 후에 str이 새 문자열을 참조하게 된다.
  • 그래서 기존에 hello를 참조하던 str이 hello world를 참조하게 된다.
Integer a = 10;
a = a + 1;
  • 위 String과 원리는 동일하다.

불변이라는 제약으로 부작용이라는 큰 재앙을 막을 수 있게 됐다!

수정은 어떻게?

생성자를 통해서만 값을 세팅할 수 있기 때문에 통으로 갈아내야 한다.

Address address = new Address("city", "address", "10000");

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

Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);


임베디드 타입

@Entity
public class Member {

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

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

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private String city;
    private String street;
    private String zipcode;
    
    ... getter and setter
}
  • 👆 위와 같은 Member에 임베디드 타입을 적용하면 아래와 같다.
@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    ... getter and setter
}
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
    
    ... getter and setter
}
@Entity
public class Member {

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

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

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;
    
    ... getter and setter
}
Member member = new Member();
member.setUsername("hello");
member.setHomeAddress(new Address("city", "street", "address"));
member.setWorkPeriod(null);

em.persist(member);
  • 새로운 값 타입을 정의해서 사용한다. 즉, 임베디드 타입도 값 타입이다.
  • int, String과 같은 값 타입을 모아서 만든다.
  • @Embeddable : 값 타입을 정의하는곳에 표시
  • @Embedded : 값 타입을 사용하는곳에 표시
  • 임베디드 타입을 사용하든 안하든, 매핑하는 테이블 구조는 동일하다. 매핑만 잘 해주면 된다.
  • 결국, 임베디드 타입은 엔티티의 값일 뿐이다.
  • 엔티티 클래스와 마찬가지로, 생성자를 별도로 정의하고 싶으면 반드시 파라미터 없는 기본 생성자도 함께 정의해야 한다. 생성자를 별도로 정의하고 싶지않으면 기본생성자를 정의 안해도된다. 이것은 생성자를 별도로 정의하지않으면 자바 특성상 기본 생성자를 자동으로 생성해준다.
  • 임베디드 타입의 값이 null이면(=member.setWorkPeriod(null)) 매핑한 컬럼(startDate, endDate)의 값은 모두 null이 들어간다.

💡한 엔티티에서 같은 임베디드 타입을 사용하면?

@Entity
public class Member {

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

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

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

=> 컬럼명이 중복되서 오류가 발생한다. 그래서 @AttributeOverrides를 사용해서 컬럼명 속성을 재정의 해야한다.



값타입 컬렉션

cf)
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

  • @JoinColumn(name = "TEAM_ID")
    @ManyToOne과 같은 연관관계 매핑 어노테이션과 사용하는 @JoinColumn
    = 매핑할 외래 키 컬럼 이름을 적는다. 테이블에서 외래키로 사용되는 컬럼이름을 적는다. @JoinColumn을 통해 Member엔티티의 team 필드와 MEMBER 테이블의 외래키 컬럼을 매핑한다.
    (즉, 객체의 참조와 테이블의 외래키를 매핑한다.)

@Entity
public class Member {

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

	@ElementCollection
	@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
	private List<Address> addressHistory = new ArrayList<>();
}
  1. db에는 컬렉션을 같은 테이블에 저장할 수 없기때문에 컬렉션을 저장하기 위한 별도의 테이블이 필요하다. @ElementCollection에 의해 테이블이 생성되고, @CollectionTable(name = "ADDRESS")를 통해 테이블명을 지정한다.
  2. 값타입컬렉션이 어떤 엔티티에 속해 있는지 JPA가 알 수 있도록 해야한다.
  3. Member는 엔티티 타입이고, Address는 값 타입이고 여러개이므로 별도의 테이블에 저장한다. 하지만 Address는 엔티티가 아니라서 @Id가 없다. 그래서 Address가 어느 Member에 속해 있는지를 알수 있게 joinColumns = @JoinColumn(name = "MEMBER_ID")를 통해 지정해준다.
  4. ADDRESS테이블에 MEMBER_ID 외래키 컬럼이 생성되고 MEMBER_ID를 통해 어느 Member에 속해있는지 알 수 있다.
  5. 참고로 joinColumns를 사용하지않더라도, JPA가 기본적으로 ADDRESS테이블에 member_id라는 이름으로 외래키 컬럼을 생성한다. (엔티티의 클래스명_엔티티의 기본키필드명)

저장

Member member = new Member();
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); // 값 타입 컬렉션 저장

tx.commit();
  • member만 저장해도, FAVORITE_FOOD테이블과 ADDRESS테이블에 데이터가 저장된다.
  • 값 타입 컬렉션은 Member에 있는 필드이므로, JPA가 Member를 persist할때 자동으로 함께 저장된다. 값 타입 및 값 타입 컬렉션은 소유 엔티티의 생명주기에 따라 함께 관리된다. 즉, 영속성 전이 + 고아객체 제거 기능을 가진다고 볼 수 있다.
  • 그래서 em.persist(member)를 하면 MEMBER테이블에 데이터를 저장하고 FAVORITE_FOOD테이블과 ADDRESS테이블에 데이터가 저장된다.

조회

Member member = new Member();
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); // 값 타입 컬렉션 저장

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

System.out.println("===============START============");
Member findMember = em.find(Member.class, member.getId());
System.out.println("===============지연로딩============");
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory){
	System.out.println("address = " + address.getCity());
} // 값 타입 컬렉션 조회
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods){
	System.out.println("favoriteFood = " + favoriteFood);
}

일반적인 값 타입은 즉시로딩개념이지만, 값 타입 컬렉션은 지연로딩개념이다.
그래서 Member findMember = em.find(Member.class, member.getId());만 했을때는 아래와 같은 쿼리를 발생시킨다.

select
     member0_.MEMBER_ID as member_i1_4_0_,
     member0_.city as city2_4_0_,
     member0_.street as street3_4_0_,
     member0_.zipcode as zipcode4_4_0_,
     member0_.USERNAME as username5_4_0_ 
from
     Member member0_ 
where
     member0_.MEMBER_ID=?
  1. Member의 데이터를 조회해서 Member객체를 생성한다. 값타입컬렉션의 데이터는 조회하지않고 Member 데이터만 조회한다.
  2. 이때 조회된 Member객체의 값타입컬렉션은 프록시 컬렉션 객체이다. 즉 addressHistory와 favoriteFoods는 프록시 컬렉션 객체를 참조한다.
  3. 이후에 for (Address address : addressHistory)을 호출하면 쿼리가 실행되고, 프록시 컬렉션 객체가 초기화된다.
    ===============지연로딩============
select
    addresshis0_.MEMBER_ID as member_i1_0_0_,
    addresshis0_.city as city2_0_0_,
    addresshis0_.street as street3_0_0_,
    addresshis0_.zipcode as zipcode4_0_0_ 
from
    ADDRESS addresshis0_ 
where
    addresshis0_.MEMBER_ID=?
address = old1
address = old2
select
    favoritefo0_.MEMBER_ID as member_i1_2_0_,
    favoritefo0_.FOOD_NAME as food_nam2_2_0_ 
from
    FAVORITE_FOOD favoritefo0_ 
where
    favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자  

수정

값 타입 수정

findMember.getHomeAddress().setCity("newCity"); 
// 값 타입을 수정할땐, 이렇게 해도 되긴하지만,
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); 
// 이런식으로 해야한다. 새로운 인스턴스를 생성해서 기존 인스턴스를 교체해야한다.
  • 값 타입은 JPA에서 엔티티가 아니기 때문에 변경 여부가 감지되지않는다. 그래서 값 타입의 필드 값을 setCity()처럼 직접 바꾸면 JPA는 무슨 값이 바뀌었는지 모른다. JPA는 엔티티가 아닌 객체에 대해 변경여부를 감지하지않기 때문이다.
    그래서 완전히 새로운 Address 객체를 생성해서 기존 값을 대체함으로써 변경을 JPA가 인식하게 한다. 값 타입은 참조가 바뀌면 JPA가 변경 여부를 감지 하고 update 쿼리를 실행한다.
  • 참고로 값 타입중에서 String이나 Integer같은 타입말고 객체타입이나 임베디드 타입만 해당된다.
    test.setName("newName"); // 이렇게 해도 된다.
    Integer, String같은 기본값 타입은 자바에서 제공하는 대표적인 불변객체이다. 값을 변경하면 참조가 변경되고, JPA가 변경 여부를 감지하고 변경된 값에 대해서 update쿼리를 실행한다.

값타입 컬렉션 수정

// 값 타입 컬렉션을 수정할땐, 기존 데이터를 삭제하고 새로운 데이터를 추가해야한다.
findMember.getAddressHistory().remove(new Address("old1", "street", "zipcode"));
findMember.getAddressHistory().add(new Address("new1", "street", "zipcode"));
  • JPA는 값 타입 컬렉션에 대해서 remove()나 add()할떄 equals()와 hashCode()를 사용한다. Set은 중복을 허용하지않는 컬렉션이기때문에 add()할때 이미 같은 값이 있는지 확인하기 위해 equals()와 hashCode()를 사용하고, List는 중복을 허용하는 컬렉션이기때문에 add()할떄는 단순히 추가하기만 하므로 equals()와 hashCode()는 사용되지않는다. 하지만 remove()할때는 Set은 equals()비교와 hashCode()로 삭제할 대상을 찾고, List는 equals()비교로 삭제할 대상을 찾는다. 즉, 내부적으로 equals()와 hashCode()를 기반으로 비교해서 동작한다. 그래서 반드시 equals()와 hashCode()를 필수로 재정의해야한다.
  • 정확히 같은 필드값을 가진 새로운 Address 객체를 생성해서 remove()해야 기존에 있던 Address 객체가 삭제된다.
  • 즉, 기존의 값을 가진 객체를 equals() 및 hashCode()로 찾아서 삭제하고 새로운 인스턴스를 추가한다.
  • 예를들어 addressHistory.remove(new Address("old1", "street", "zipcode")); 이 코드가 동작하려면, Address("old1", "street", "zipcode")가 기존에 리스트에 들어 있던 인스턴스와 값이 같다고 판단되어야 하고, 그 판단 기준은 equals()이다.
    또한, Set<Address>를 사용할 경우, hashCode()도 같아야 같은 객체로 인식된다.
  • equals()와 hashCode()를 재정의 할때는 기본적으로 IDE에서 재정의 해주는 그대로 사용하면된다. 모든 필드를 기준으로 equals()와 hashCode()를 구현하면된다.
  • equals()와 hashCode()를 재정의하지않으면 add()나 remove()가 정상동작하지않을수도 있다.
@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);
}
  • 참고로 String이나 Integer, Long 등등 은 이미 자바에서 기본적으로 equals()와 hashCode()가 재정의되어있다. 그래서 별도의 equals()/hashCode() 재정의 없이도 Set<String> 이나 List<String>의 add()나 remove()가 정상 동작한다.


참고로, 값타입 컬렉션은 remove()후 add()를 하게되면 JPA가 delete쿼리와 insert쿼리를 실행한다. 하지만 값 타입 컬렉션의 값의 타입에 따라 delete쿼리와 insert쿼리의 동작 방식이 다르다.
1. 값 타입 컬렉션에 들어있는 값의 타입이 기본값 타입
Integer, String같은 기본값 타입은 자바에서 제공하는 대표적인 불변객체이므로 JPA가 변경 여부를 감지하고 삭제된 값에 대해서만 delete쿼리를 실행하고, 추가된 값에 대해서만 insert쿼리를 실행한다.
delete from favorite_foods where member_id = ? and FOOD_NAME=?
insert into FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) values (?, ?)
2. 값타입 컬렉션에 들어있는 값의 타입이 객체타입이나 임베디드타입
먼저, 해당 값 타입 컬렉션에 들어있는 값을 전부 delete하고, 현재 컬렉션에 있는 전체 값들을 각각 insert한다.
delete from address where member_id = ?
insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)
insert into address (member_id, city, street, zipcode) values (?, ?, ?, ?)


참고로, Member만 영속상태인데 값 타입과 값 타입 컬렉션이 수정되면 쿼리가 발생하고 db에 반영되는 이유가 무엇일까?
=> 값 타입 및 값 타입 컬렉션도 Member 엔티티의 필드이기 때문이다. 따라서 JPA는 Member만 영속 상태여도, 그 필드인 값 타입과 값 타입 컬렉션까지도 함께 관리한다. 그래서 값타입수정 및 값타입컬렉션수정을 통해 참조를 변경해서 이 필드들에 변경이 감지되면, JPA는 DB에 반영하기위해 쿼리를 실행한다.
값 타입 및 값 타입 컬렉션은 소유 엔티티의 생명주기(영속 상태)에 따라 함께 관리된다. 따라서 소유 엔티티(Member)가 영속 상태이고, 값타입수정 및 값타입컬렉션수정을 통해 참조를 변경해서 값 타입 또는 값 타입 컬렉션에 변경이 감지되면, JPA는 DB에 반영하기 위해 쿼리를 실행한다. 즉, JPA는 값 타입 및 값 타입 컬렉션을 포함한 Member 전체를 하나의 단위로 관리한다.


값 타입 컬렉션 보다는 일대다 연관관계를 위한 엔티티를 만들고, 이 엔티티에서 값 타입을 사용하는것이 낫다.

@Entity
@Table(name = "ADDRESS")
public class AddressEntity { // 엔티티
	@ID @GeneratedValue
	private Long id;
	
	private Address address; // 값 타입

	... getter and setter
}

그래서 Member 클래스에서 일대다 연관관계를 사용한다.

@Entity
public class Member{
	...

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

      @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
      @JoinColumn(name = "MEMBER_ID")
      private List<AddressEntity> addressHistory = new ArrayList<>();
	
	...

}
Member member = new Member();
member.setUsername("member1");

member.getAddressHistory().add(new AddressEntity("old1", "street1", "zipcode1"));
member.getAddressHistory().add(new AddressEntity("old2", "street2", "zipcode2"));
member.getAddressHistory().add(new AddressEntity("old3", "street3", "zipcode3"));

em.persist();

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

Member findMember = em.find(Member.class, member.getId());
AddressEntity addressEntity = findMember.getAddressHistory().get(0);
addressEntity.setAddress(new Address("new1", "street1", "zipcode1")); // 값타입 수정(=참조변경)
  1. 이렇게 하면 기존 delete로 다 삭제하고 insert를 각각 해주는걸 단순히 update쿼리 하나로 해당 데이터만 수정하게 하기위해 사용한다.
    결국 AddressEntity 엔티티 안에 있는 값타입을 수정하는것이기 때문에 값 타입을 수정하는것처럼 addressEntity.setAddress(new Address("new1", "street1", "zipcode1")); 이렇게 해주면된다.
    AddressEntity는 엔티티이기때문에 JPA가 변경여부를 감지하긴 하지만, 값 타입은 JPA에서 엔티티가 아니기 때문에 변경 여부가 감지되지 않는다. 그래서 필드 값을 setCity()처럼 직접 바꾸면 JPA는 무슨 값이 바뀌었는지 모른다. JPA는 엔티티가 아닌 객체에 대해 변경여부를 감지하지않기 때문이다.
    즉, 엔티티의 값타입컬렉션 필드의 값이 수정(=삭제되고 추가)되면, 참조가 변경되었으므로 JPA가 변경을 감지해서 수정쿼리가 발생하는것이고, 엔티티의 값타입 필드의 값이 수정(=참조변경)되면, 참조가 변경되었으므로 JPA가 변경을 감지해서 수정쿼리가 발생하는것이다.
    그리고, addressEntity의 필드(=여기서는 값타입 필드)의 값이 수정되면 Member엔티티와는 상관없이 AddressEntity엔티티의 필드의 값이 수정됬기 때문에 JPA가 변경을 감지해서 수정 쿼리가 발생하는것이다. Member가 엔티티라서 변경여부를 감지하는게 아니라, AddressEntity가 엔티티라서 변경여부를 감지하는것이다.

  2. 다대일 양방향 연관관계를 사용하지않고 일대다 단방향 연관관계를 사용한 이유
    => AddressEntity에서 Member에 대해서 조회할 일이 없고 Member에서만 AddressEntity에 대해서 조회할 일이 있으므로

  3. cascade와 orphanRemoval을 사용하는 이유
    => cascade는 특정 엔티티를 저장하거나 삭제할때, 연관된 엔티티(=연관관계 매핑 @OneToMany, @ManyToOne 등을 통해 서로 연결되어 있는 엔티티)에도 같은 작업을 하도록 설정하는 기능이다. 그래서 em.persist(member)를 하면 member.getAddressHistory()의 주소들도 같이 저장된다. 그리고 orphanRemoval을 사용하면 Member의 addressHistory에서 어떤 AddressEntity를 제거하면, 해당 AddressEntity가 db에서 자동으로 삭제된다.
    즉, cascade를 사용하지않으면 별도로 주소엔티티를 저장해야된다. orphanRemoval을 사용하지않으면 컬렉션에서만 제거되고, db에는 남아있어서 데이터 불일치가 발생할 수 있다.

  4. 참고로 엔티티클래스에 생성자를 정의하고 싶으면 반드시 파라미터 없는 기본 생성자도 함께 정의해야 한다. 이는 JPA 스펙에 의한 필수 조건이다.
    생성자를 별도로 정의하고 싶지않으면 기본생성자를 정의 안해도된다. 이것은 생성자를 별도로 정의하지않으면 자바 특성상 기본 생성자를 자동으로 생성해준다.

0개의 댓글