
이 글은 우테코 프리코스 오픈미션을 진행하던 중 JPA의 벽에 부딪혀 공부한 내용을 정리한 글이다. 그 중에서도 임베디드 타입(Embedded Type) 에 대해 집중적으로 다뤘다.
이전에는 아무것도 모르고 단순히 다들 사용하니깐 해야지라고 생각했다. 하지만 이번에는 직접 찾아보고 공부하고 사용해보며 JPA가 데이터를 다루는 두 가지 타입을 이해하게 되었다.
특히 순수 자바로 객체 간 협력을 설계할 때와 스프링에서 JPA를 통해 협력할 때의 개념적 차이를 많이 느꼈다. (자세한 내용은 마지막에 설명하겠다.)
엔티티(Entity) : 식별자(@Id)가 있어서 변화를 추적할 수 있는 객체
ex) @Entity
값 타입(Value Type) : 고유 식별자가 없고 그 자체로 값으로만 존재하는 객체
ex) @Embeddable, @Embedded
엔티티는 데이터베이스의 테이블과 직접 연결되는 객체다.
식별자(@Id)가 있고, 데이터가 바뀌더라도 같은 식별자를 가지면 같은 객체로 인식된다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
@Entity : 이 클래스는 DB 테이블에 매핑된다는 뜻@Id : 이 필드는 기본 키(Primary Key)@GeneratedValue : 자동 증가 설정값 타입은 엔티티의 생명주기에 종속되는 객체다.
엔티티가 삭제되면 함께 삭제되고, JPA에서 변경 추적이 불가능하다.
엔티티와 달리 식별자가 없으며,
데이터가 바뀌면 그건 “다른 값”으로 간주된다.
@Embeddable // 값 타입 정의
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded // 값 타입 포함
private Address address;
}
값 타입 안에서도 세 가지로 나뉜다.
기본값 타입 : 자바 기본 타입이나 래퍼 타입
ex) int, String, Boolean
임베디드 타입(Embedded Type) : 여러 값을 묶어 하나의 의미로 표현
ex) Address, Period, Money
값 타입 컬렉션(Collection Value Type) : 값 타입을 여러 개 보관하는 컬렉션
ex) List<Address>, Set<String>
그냥 자바의 기본 자료형이다.
DB와 바로 매핑되기 때문에 별다른 어노테이션이 필요 없다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name; // 기본값 타입
private int age; // 기본값 타입
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Embedded
private Address address;
}
사실 city, street, zipcode를 Member 엔티티에 바로 넣어도 된다.
하지만 이렇게 하면 ‘주소’라는 개념이 코드 곳곳에 흩어져 응집력이 떨어지고, 유사한 필드가 여러 엔티티에서 중복 선언될 가능성이 생긴다.
이를 해결하기 위해 JPA는 임베디드 타입(@Embeddable / @Embedded) 을 제공한다.
- DB 테이블은 그대로, 객체만 논리적으로 분리된다.
- 한 번 만들어두면 여러 엔티티에서 재사용 가능하다.
- 관련된 필드를 묶어 응집력을 높이고 의미 있는 단위로 다룰 수 있다.
- 내부 메서드를 통해 의미 있는 값을 도출할 수 있다.
여러 필드를 묶어 하나의 단위로 다루는 복합 값 타입이다.
결과적으로 Member 테이블 하나만 생성되고 Address 필드들이 같은 테이블에 함께 매핑된다.
| id | name | city | street | zipcode |
|---|---|---|---|---|
| 1 | 철수 | 서울 | 강남로 | 12345 |
값 타입을 여러 개 가질 때 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ElementCollection
@CollectionTable(name = "favorite_city", joinColumns = @JoinColumn(name = "member_id"))
private List<String> favoriteCities = new ArrayList<>();
}
JPA는 식별자(@Id) 를 기준으로 변경을 추적한다.
하지만 값 타입은 식별자가 없기 때문에 “어떤 값이 바뀌었는지”를 알 수 없다.
그래서 값이 바뀌면 JPA는 안전하게 전체 삭제 후 새로 삽입한다.
member.getAddress().setCity("Busan"); // 감지되지 않음
member.setAddress(new Address("Busan", "해운대", "6623")); // 교체로 인식
이런 특성 때문에 값 타입은 불변 객체로 설계하는 것이 일반적이다. setter를 없애고, 생성자로만 값을 설정해 사이드 이펙트를 방지한다.
값 타입 컬렉션은 단순하지만, 부분 수정이나 상태 추적이 필요한 경우에는 부적합하다.
이럴 땐 엔티티 컬렉션(@OneToMany) 으로 설계해야 한다.
아래는 내가 실제로 사용했던 코드 예시다.
(처음에 @Embeddable만 사용하고 @OneToMany를 사용하지 않아서 오류가 생겼었다... 아래는 해결한 코드이다.)
@Entity
public class StoredBook {
@Id @GeneratedValue
private Long id;
private String code;
private String status;
}
@Embeddable
public class StoredBooks {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<StoredBook> storedBooks = new ArrayList<>();
}
@Entity
public class Book {
@Id @GeneratedValue
private Long id;
private String title;
@Embedded
private StoredBooks storedBooks = new StoredBooks();
}
이 구조를 사용하면
Book을 저장하면 StoredBook도 함께 저장되고 (cascade = ALL),Book에서 제거하면 StoredBook도 DB에서 삭제되며 (orphanRemoval = true)StoredBook은 독립적인 엔티티로 변경 추적이 가능하다.이전에는 @Entity 와 @Embedded 의 차이를 모르고 사용했지만,
이번 오픈미션을 계기로 “이 데이터가 진짜 값인지, 아니면 엔티티인지”를 판단하는 것이 설계의 핵심이라는 걸 배웠다.
JPA는 단순히 데이터를 저장하는 기술이 아니라,
객체의 관계와 생명주기를 어떻게 설계할 것인지 스스로 고민하게 만드는 도구였다.
그리고 한 가지 더 느낀 점은,
순수 자바에서의 객체 협력과 스프링 JPA에서의 객체 협력은 전혀 다르다는 것이다.
순수 자바로 우테코 프리코스 미션을 진행할 때는 객체가 서로 메시지를 주고받으며 책임을 수행하고, 협력 구조를 명시적으로 드러내는 방식으로 설계했다.
하지만 스프링 JPA로 넘어오자 객체 협력이 단순한 호출 관계가 아니었다.
엔티티 간의 관계는 어노테이션으로 정의되고 값 타입을 다루는 방식이나 일급 컬렉션의 생성조차 JPA의 규칙을 따라야 했다.
순수 자바에서 객체들이 자유롭게 메시지를 주고받던 때와 달리
여기서는 영속성 컨텍스트와 데이터베이스라는 규칙 안에서 간접적으로 협력하는 구조였다.
그 변화가 새로우면서도 낯설었고, 동시에 신기했다.
그 과정에서 “JPA가 단순한 ORM이 아니라 객체와 데이터의 세계를 연결하는 기술”이라는 걸 온몸으로 느꼈다.
앞으로도 이런 차이를 이해하며 기술을 단순히 사용하는 개발자가 아니라 이해하고 설계하는 개발자로 성장하고 싶다.
JPA #9 값 타입, 컬렉션, 임베디드 타입
PA Embedded, Embeddable, 속성의 재정의
JPA 사용시 @Embedded 주의사항