JPA 시리즈 2편
2. 엔티티와 값 타입
JPA를 처음 접하면 모든 클래스가 비슷하게 느껴집니다. 예를 들면, 회원 클래스도 있고, 주소 클래스도 있고, 주문 클래스도 있습니다. 겉보기에는 다 그냥 객체처럼 보이지만, JPA는 이들을 같은 방식으로 다루지 않습니다. 어떤 객체는 따로 관리하고, 어떤 객체는 다른 것에 붙어서 함께 움직입니다. 이 차이를 이해하기 시작하면 JPA가 점점 명확하게 다가오게 됩니다.
처음에 클래스를 설계할 때는 뭔가 다 비슷해 보입니다. 회원 정보를 위한 Member 클래스, 주소를 담는 Address 클래스, 그리고 주문을 나타내는 Order 클래스가 있죠. 자바 코드만 봐서는 전부 그냥 객체이고, 필드도 있고 생성자도 있고 값을 넣어 쓸 수 있습니다. 그래서 아마 처음엔 “이 정도면 다 같은 객체니까 JPA도 전부 똑같이 저장하면 되겠지?”라고 쉽게 생각할 수 있습니다.
하지만 JPA의 생각은 다릅니다. JPA 안에서 객체는 두 가지로 나뉩니다. 하나는 '엔티티', 그리고 다른 하나는 '값 타입'입니다. 겉으로는 비슷해 보여도, 두 가지는 의미 자체가 완전히 다릅니다.
처음부터 이걸 외우듯이 복잡하게 받아들이면 쉽게 헷갈릴 수 있습니다. 그래서 쉽게 접근하려면 JPA가 어떤 객체를 볼 때 이렇게 질문한다고 생각해보면 좋습니다. “이 객체는 스스로 존재하는 주체인가? 아니면 다른 객체에 붙어서만 의미가 있는 값인가?” 이렇게 나눠보면 이해가 한결 쉬워집니다.
회원은 바뀌어도 같은 회원이다
예를 들어, 회원 하나를 떠올려 보세요. 시간이 지나면 회원의 이름도 바뀔 수 있고, 전화번호나 주소가 바뀔 수도 있습니다. 하지만 이름이나 정보가 바뀌었다고 해서 그 사람이 완전히 다른 사람이 되는 건 아닙니다. 여전히 같은 회원이죠. 우리는 보통 회원을 회원번호나 id 같은 식별자로 구분합니다.
이렇게 식별자가 있는 대상이 바로 엔티티입니다.
엔티티는 내용이 조금 변해도 같은 존재로 취급합니다. 예를 들어, 이름이 김철수였다가 김민수로 바뀌어도 회원 id가 같다면 그 사람은 같은 회원입니다. JPA는 이런 대상을 id를 기준으로 관리합니다.
@Entity
@Table(name = "members")
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
protected Member() {
}
public Member(String name) {
this.name = name;
}
public void changeName(String name) {
this.name = name;
}
}
여기서 중요한 건 name이 아니라 id입니다. 이름은 얼마든지 바뀔 수 있지만, JPA는 이 회원을 id로 구분합니다.
그래서 엔티티를 이해할 때는 이런 질문을 한 번 던져보면 좋아요. “이 객체는 뭐가 바뀌어도 여전히 같은 대상이라고 할 수 있을까?” 만약 그렇다면, 이건 엔티티일 확률이 높아요.
주소는 다르게 생각해야 해요
이번에는 주소에 대해 한번 생각해 볼게요. 서울, 강남, 어떤 도로명, 우편번호 같은 정보들요. 주소는 회원처럼 ‘누구’를 나타내는 대상이 아니에요. 그냥 값, 즉 정보 자체일 뿐이거든요. 그리고 이 주소가 어디에 속하는지에 따라 의미가 달라져요. 회원의 주소일 수도 있고, 배송지 주소일 수도 있고요. 주소만 따로 떼어 놓으면, 뭔가 독립적으로 존재감이 있는 대상이라기보다는, 다른 엔티티에 붙어 있는 정보 같은 느낌이에요.
이런 게 바로 값 타입이라고 부르는 거죠.
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
protected Address() {
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
}
보통 주소에는 따로 식별자를 두지 않아요. 주소 id 같은 걸 만들어서 하나씩 구분하지 않는다는 거죠. 주소에서 더 중요한 건 그 값, 즉 서울인지, 부산인지, 어느 거리인지 같은 내용이에요. 어떤 id를 가졌는지가 아니라 무엇을 담고 있는지가 더 중요하다는 거예요.
결국 값 타입에서는 정체성보다는 '내용'이 훨씬 중요해요.
엔티티와 값 타입, 왜 굳이 나눠야 할까?
처음에는 이런 고민이 들 수도 있어요. “그냥 다 엔티티로 만들면 되지 않을까? 다 id를 하나씩 붙이고, 테이블로 빼 놓으면 편하지 않을까?” 네, 기술적으로는 그렇게 할 수도 있지만, 실제로 해보면 오히려 모델이 더 어색해질 때가 많아요.
예를 들면 회원과 주소는 확실히 느낌이 서로 달라요. 회원은 독립적으로 조회하고, 수정하고, 별도로 관리해야 할 대상이에요. 반면에 주소는 보통 회원에 붙어서 같이 다녀요. 회원을 저장할 때 주소도 함께 들어가고, 회원 정보가 바뀌면 주소도 같이 바뀌는 경우가 대부분이거든요. 주소만 따로 꺼내서 그 생명 주기를 독립적으로 관리하는 경우는 거의 없죠.
JPA의 좋은 점 중 하나가 바로 이런 차이를 자연스럽게 담아낼 수 있다는 거예요. 독립적으로 존재해야 하는 대상은 엔티티로, 특정 엔티티에 딸려 붙는 정보는 값 타입으로 표현해주면 되니까요. 이렇게 나누면 클래스 구조도 훨씬 자연스럽고요.
값 타입은 늘 주인을 갖고 있어요
값 타입을 이해할 때 가장 중요한 감각, 바로 이거예요. 값 타입은 혼자서 존재하지 않아요. 꼭 누군가(즉, 엔티티)에게 소속되어 있어요.
예를 들어, 회원이 주소를 가진다고 하면 어떤 느낌일지 감이 오시나요?
@Entity
@Table(name = "members")
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
private Address address;
protected Member() {
}
public Member(String name, Address address) {
this.name = name;
this.address = address;
}
}
이 구조에서 주소라는 것은 회원한테 딱 달라붙어 있는 값이에요. 그냥 혼자 독립적으로 남지 않고, ‘회원의 주소’라는 방식으로만 존재하는 거죠. 그래서 JPA에서는 @Embeddable이랑 @Embedded를 써서 이런 값 타입을 표현하게 되어 있어요.
그리고 궁금할 수 있는데, 이게 실제로 DB에 저장될 때 주소가 꼭 별도의 테이블로 빠지는 건 아니에요. 대부분은 회원 테이블 안에 '도시', '거리', '우편번호' 같은 컬럼으로 나란히 저장돼요. 즉, 자바에서는 Address 객체 하나처럼 보이지만, 실제 DB에는 다양한 컬럼으로 쪼개져서 들어가는 식이죠.
처음 JPA를 접하면 이 부분이 좀 신기하게 느껴질 수 있더라고요. 자바 코드에서는 분명 하나의 객체로 다루는데, DB에서는 그 값들이 회원 테이블 안에 스며들어 있는 구조라서요. 근데 곰곰이 생각해 보면, 오히려 이게 훨씬 자연스럽지 않나요? 애초에 주소라는 건 회원한테 붙어다니는 값이니까요.
엔티티는 공유해도 되지만, 값 타입은 조심하세요!
여기서 저도 예전에 많이 헷갈렸던 부분이 등장해요. 엔티티는 여러 군데에서 마음껏 참조해도 괜찮아요. 예를 들어 회원 엔티티는 주문에서도, 리뷰에서도 그대로 가져다 쓸 수 있죠. 왜냐하면 엔티티는 ‘식별자’라는 게 있으니까요, 독립적으로 존재하는 대상인 거죠.
반대로 값 타입은 개념이 좀 다릅니다. 값 타입은 오로지 내용이 중요하고, 주로 주인 엔티티에 매달려서 다니는 정보예요. 그래서 같은 Address 객체를 여러 곳에서 같이 써버리면, 생각지도 못한 이상한 상황이 발생할 수 있어요. 정말 조심해야겠죠?
Address address = new Address("Seoul", "Gangnam", "12345");
Member member1 = new Member("kim", address);
Member member2 = new Member("lee", address);
겉으로 보면 아무 문제 없어 보여요. “같은 주소인데 주소 객체 하나를 같이 써도 괜찮지 않을까?” 싶은데요. 그런데 만약 한쪽에서 주소를 바꾸면 어떻게 될까요? 두 멤버가 같은 주소 객체를 공유하고 있으니까, 어느 한 곳에서 바꿔버리면 다른 쪽에도 그 변화가 그대로 반영돼요. 의도하지 않은 일이 벌어질 수 있는 거죠.
이런 상황에서 값 타입이 엔티티랑 관점이 완~전히 다르다는 걸 알 수 있어요. 엔티티는 서로 참조를 공유해도 괜찮은 경우가 많은데, 값 타입은 공유보다는 복사해서 따로 쓰는 게 안전하답니다. 그러니까 “남이 바꿔서 내 것도 바뀌는 상황”을 애초에 막아주는 거죠!
그래서 값 타입 쓸 때는 기존 걸 직접 고치기보단 아예 새로운 걸 만들어 바꿔 끼우는 방식이 보통 더 자연스럽고 실수도 줄여줘요. 예를 들면, 주소를 바꿔야 한다면 아래처럼 최신 정보를 담은 새 Address를 만들어서 갈아주는 거죠.
member.changeAddress(new Address("Busan", "Haeundae", "54321"));
이 방식, 솔직히 처음엔 좀 귀찮게 느껴질 수 있어요. 그래도 ‘값’이란 개념에 참 딱 어울립니다. 주소 자체가 누굴 식별하는 게 아니라, 안에 어떤 정보가 담겼는지가 중요하니까요.
엔티티는 동일성, 값 타입은 동등성을 본다
이 문장, 처음 들으면 살짝 딱딱하게 느껴지죠? 근데 생각보다 어렵지 않아서 금방 이해돼요.
엔티티는 “이게 예전 그 사람 맞냐?”처럼 동일성에 집중합니다. 회원 A와 회원 B가 이름이 똑같아도, id가 다르면 완전히 다른 회원이에요. 반대로, 이름은 바뀌어도 id만 같으면 여전히 그 사람인 거고요.
반면 값 타입은 ‘속에 든 값’이 같은지, 즉 동등성만 따져요. 예를 들어, “서울, 강남, 12345” 이게 완벽하게 같으면 그냥 같은 ‘주소 값’입니다. 주소는 누군가처럼 독립적으로 뭔가를 대표하는 게 아니라, 그 내용 자체가 핵심이에요.
이 차이, 머리로만 외우지 말고 일상 감각으로 익혀두면 훨~씬 쉽답니다. 회원은 사람을 가리키고, 주소는 정보 조각을 가리킨다고 생각해보세요. 사람은 이름이 같아도 다른 사람일 수 있지만, 주소는 내용만 같으면 같은 주소라고 보면 되니까요 :)
왜 초보자일수록 이 구분을 빨리 알아야 할까
처음에는 솔직히 이 차이를 잘 몰라도 코드가 돌아가긴 해요. 그냥 클래스에 @Entity만 붙이고 저장하면 얼핏 시스템이 잘 굴러가는 것처럼 보이죠. 그래서 엔티티랑 값 타입 구분이 별로 중요해 보이지 않을 수도 있어요.
근데 조금만 지나면 구조가 조금씩 어색해지기 시작하더라고요. 단지 값 하나만 표현하려고 쓸데없이 id를 만들어야 하고, 새로운 테이블까지 추가하고... 반대로 분명히 독립적으로 다뤄야 할 대상을 그냥 필드 몇 개로 퉁쳐버리는 경우도 생겨요. 이렇게 넘어가다 보면, 코드가 처음엔 편하게 보여도 시간이 지나면 점점 의미가 흐릿해져요.
예를 들어 주소는 사실 값인데, 엔티티처럼 다뤄버리면 나중에 주소 정보를 별도로 조회하거나 수정하고, 별도 연결까지 하게 되면서 모델 규모가 괜히 커질 수 있어요. 반대로 회원처럼 독립적으로 관리해야 할 대상을 단순 값처럼만 다루면, 식별자나 생명주기도 제대로 표현할 수 없게 되죠. 그래서 이 구분을 미리 제대로 알고 있으면, 나중엔 정말 일이 편해져요. 저도 처음엔 무시했는데, 시간이 갈수록 절실하게 느꼈거든요.
처음에는 이렇게 구분하면 된다
처음 배울 때는 너무 어렵게 생각하지 않아도 돼요. 아래 기준만 가볍게 잡아도 시작하기에 충분하다고 생각해요.
어떤 객체가 스스로 대표할 수 있는 id가 있고, 다른 객체와 관계도 맺고, 오랜 시간이 흘러도 같은 대상을 계속 추적해야 한다면 그건 엔티티 쪽에 가까워요. 반대로, 객체가 단순히 데이터의 묶음이고, 다른 엔티티 내부에서만 의미가 있다면, 그리고 값 그 자체가 더 중요하다면 그건 값 타입이에요.
이 기준으로 보면 자연스럽게 회원, 주문, 상품, 팀 이런 게 다 엔티티라 생각하기 쉬워요. 주소, 기간, 금액 이런 건 값 타입이구요. 실제로 설계하다 보면 더 고민해야 하는 상황도 있지만, 저는 이 기준만 기억해도 초반에는 훨씬 그림이 그려지더라고요.
정리
JPA 안에서 모든 객체가 다 똑같은 건 아니에요. 어떤 건 엔티티이고, 또 어떤 건 값 타입이죠. 엔티티는 식별자를 갖고 독립적으로 관리되는 대상이고, 값 타입은 다른 엔티티 안에 소속되어서 의미를 갖는 정보 묶음이에요.
예를 들면 회원은 이름이 바뀌어도 언제나 그 회원 그대로지만, 주소는 내용이 같으면 같은 주소 값이라고 볼 수 있어요. 회원은 id로 식별하고, 주소는 그 안의 실제 값으로 의미가 정해지는 거죠. 이 원리를 이해하고 나면, JPA에서 어떤 클래스에는 @Entity, 또 어떤 클래스에는 @Embeddable 이런 식으로 사용하는 이유가 눈에 들어오기 시작해요.
처음엔 그저 클래스 구분 이야기인 줄만 알았는데, 사실은 JPA가 객체를 대하는 근본적인 방식이랑 연결되는 부분이에요. 이 부분만 제대로 잡고 있으면, 그 뒤에 나오는 영속성 컨텍스트나 상태 관리 같은 것들도 훨씬 쉽게 이해할 수 있으니 꼭 한 번 정리해 두시면 좋을 것 같아요. 😊
JPA 시리즈
- 1. JPA란?
- 2. 엔티티와 값 타입
- 3. 영속성 컨텍스트
- 4. 엔티티의 생명주기
- 5. flush와 commit
- 6. 변경 감지란 무엇인가
- 7. 연관관계와 연관관계의 주인
- 8. 지연 로딩과 프록시
- 9. JPQL은 왜 필요한가
다음 글
3편에서는 영속성 컨텍스트 이야기를 해보려고 해요. JPA가 왜 굳이 조회한 객체를 가만히 두지 않고 직접 관리하려고 하는지, 그리고 왜 같은 엔티티는 무조건 같은 것으로 취급하려는지—이런 부분들을 자연스러운 흐름에 맞춰 풀어서 정리해볼게요. 궁금하시죠? 함께 살펴봐요!
'Tech Stack > JPA' 카테고리의 다른 글
| [JPA] 1.JPA란? (0) | 2026.03.13 |
|---|