DevLog ๐ถ
[JPA] @Embedded ์ฌ์ฉ ์ ์ฃผ์ํ ์ , ๋ ๊ฑฐ์ ์ฝ๋ ๋ฆฌํฉํฐ๋งํ๊ธฐ ๋ณธ๋ฌธ
[JPA] @Embedded ์ฌ์ฉ ์ ์ฃผ์ํ ์ , ๋ ๊ฑฐ์ ์ฝ๋ ๋ฆฌํฉํฐ๋งํ๊ธฐ
dolmeng2 2023. 12. 16. 10:00๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
๋๋ฌด ์ค๋๋ง์ ์์ฑํ๋ ๋ธ๋ก๊ทธ ๊ธ...! ์ต๊ทผ์ ์ฌ๋ด์์ ๋งค์ฐ ๋ง์ด ์ฐ์ด๋ ๋๋ฉ์ธ์ ๋ํด์ ๋ฆฌํฉํฐ๋ง์ ์งํํ์๋๋ฐ, ๊ฑฐ๊ธฐ์ ๋ง๋ฌ๋ ์ด์๋ค๊ณผ ๊ฐ๋จํ ์๊ฐ ๊ธฐ๋ก์ ๋ธ๋ก๊ทธ์ ๋จ๊ธฐ๋ฉด ์ข์ ๊ฒ ๊ฐ์์ ์์ฑํ๊ณ ์ ํ๋ค. ์ด๋ฏธ ํ ๋ด์์ ๊ณต์ ๋ ํ์๊ณ , ๋๊ธฐ๋ค์๊ฒ๋ ๊ณต์ ํ ๋ด์ฉ์ด๊ธฐ๋ ํ๊ณ , ์ฌ์ค ๋ณ๊ฑฐ ์๋ ๊ฑฐ๋ผ ๊ทธ๋ฅ ๊ฐ๋จํ๊ฒ๋ง ์ ๋ฆฌํ ์์ ์ด๋ค.
๐ฑ ๋ฌธ์ ์ํฉ
๋ฆฌํฉํฐ๋ง์ ์งํํ๊ณ ์ ํ ๋๋ฉ์ธ์ ์ฐ๋ฆฌ ํ์์ ๋งค์ฐ ์ค์ํ ๋น์ฆ๋์ค ๋๋ฉ์ธ ๊ฐ์ฒด ์ค ํ๋์๊ณ , ์ด๋ฏธ ์์ ์ ์ผ๋ก ์ ์ด์๋๊ณ ์์ง๋ง ๊ต์ฅํ ์๋ ๋ถํฐ ๋ ๊ฑฐ์๋ก ๋ด๋ ค์ค๊ณ ์๋ ๊ฐ์ฒด์๊ธฐ ๋๋ฌธ์ ์ ๊ท ์
์ฌ์๊ฐ ๋ณด๊ธฐ์ ์ข์ ์ฝ๋๊ฐ ์๋๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
์ฌ๋ด ๋๋ฉ์ธ์ธ ๋งํผ ๊ทธ๋๋ก ๊ฐ์ ธ์ฌ ์๋ ์์ง๋ง, ๋น์ทํ ๋๋์ผ๋ก ์๋์ ๊ฐ์ด ์ธํ
์ ์งํํด๋ณด์๋ค.
CREATE TABLE `student_group` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3
CREATE TABLE `student` (
`id` bigint NOT NULL AUTO_INCREMENT,
`city` varchar(255) DEFAULT NULL,
`county` varchar(255) DEFAULT NULL,
`district` varchar(255) DEFAULT NULL,
`age` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
`group_id` bigint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
๊ฐ๋จํ๊ฒ ์๊ฐํ๋ฉด ํ์์ ๊ทธ๋ฃน๊ณผ ํ์์ ๋ํ ํ ์ด๋ธ์ด๊ณ , ์ด์ ๋ํ ์ํฐํฐ ๋งคํ์ ์๋์ ๊ฐ์๋ค.
@Entity
class Student(
val name: String,
val age: Int,
group: StudentGroup
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L
@ManyToOne
@JoinColumn(name = "group_id", insertable = false, updatable = false)
val group: StudentGroup = group
@Column(name = "group_id")
val groupId: Long = group.id
@Column
val city: String? = null
@Column
val county: String? = null
@Column
val district: String? = null
}
์ค์ ๋๋ฉ์ธ์์๋ 30๊ฐ๊ฐ ๋๋ ํ๋๋ค์ด ์์์ผ๋ฉฐ, ๊ต์ฅํ ๋ง์ ์ผ๋ค์ ํ๊ณ ์๋ ๊ฐ์ฒด์๊ธฐ ๋๋ฌธ์ ํ์
ํ๊ธฐ ์ฝ์ง ์์ ๊ฐ์ฒด์๋ค.
๋๋ ์์ ์ํฐํฐ๋ฅผ ๋ณด๊ณ , ๋ค์๊ณผ ๊ฐ์ 2๊ฐ์ง ํฌ์ธํธ์ ์ง์คํ์๋ค.
1. group์ด๋ผ๋ ์ํฐํฐ๋ฅผ ์ด๋ฏธ ์์กดํ๊ณ ์๋๋ฐ, ๊ผญ groupId๋ผ๋ ํ๋๋ฅผ ๋ฐ๋ก ๋์ด์ผ ํ ๊น?
2. ๋งฅ๋ฝ์์ผ๋ก ์ฐ๊ด์ด ์๋ ํ๋๋ค์ @Embedded๋ก ๋ฌถ์ด๋๋ฉด ๊ฐ๋ ์ฑ์ด ์ฌ๋ผ๊ฐ์ง ์์๊น?
๋จผ์ , 1๋ฒ์ ๊ฒฝ์ฐ JPA์ ๋ํ ์์กด์ฑ์ ์ต๋ํ ๋ฎ์ถ๊ณ ์ถ์๋ค. ์ด๋ฏธ @JoinColumn์ผ๋ก ์ํฐํฐ ์ง์ ์ฐธ์กฐ๋ฅผ ํตํด ์์กด์ฑ์ ๊ฐ์ง๊ณ ์์์๋, @Column์ ํตํด ๊ฐ์ ์ฐธ์กฐ๊น์ง ๊ฐ์ง๊ณ ์๋ ๊ฒ ์ฒ์ ๋ณธ ์ฌ๋์ ์
์ฅ์์๋ ์ธ์ง ์ค๋ฅ๋ฅผ ์ค ์ ์๋ ํฌ์ธํธ๋ผ๊ณ ์๊ฐํ์๋ค. (๋ํ, ํ์ฌ๋ group_id๋ง ์์ง๋ง ์ค์ง์ ์ผ๋ก ์ํฐํฐ ์ฐธ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฐ์ฒด๋ค์ด ๋ชจ๋ ์์ ๊ฐ์ด ๋ณ๋๋ก Id๋ฅผ ์ํ ํ๋๋ค์ด ์กด์ฌํ๊ณ ์์๋ค.) ์ธ๋ป ๋ณด๋ฉด group_id๋ผ๋ ์ปฌ๋ผ์ด 2๋ฒ ์๋ ๊ฒ์ธ๊ฐ? ๋ผ๋ ์ค๋ฅ๋ฅผ ์ค ์ ์์ ๊ฒ ๊ฐ์๋ค.
2๋ฒ์ ๊ฒฝ์ฐ, ํ์ฌ ์์ ์ํฐํฐ๋ฅผ ์ค์ง์ ์ผ๋ก ๋๋ฉ์ธ ๊ฐ์ฒด๋ก ์ฌ์ฉํ๊ณ ์๋ค ๋ณด๋ ๊ต์ฅํ ๋ง์ ๋น์ฆ๋์ค ๋ก์ง์์ ์ฌ์ฉ๋๋ ๊ฐ์ฒด์๋ค. ๊ทธ๋ฌ๋ ์๋ง์ ํ๋ ๋ฐ ๋ฉ์๋๋ค์ด ์ฃผ์ ์์ด ๋จ์ ํ๋๋ช
์ผ๋ก, ํน์ ํ
์ด๋ธ ์คํค๋ง์ ์์ฑ๋ ์ปค๋ฉํธ ๋ด์ฉ์ ๋ณด๊ณ ํ์
์ ์งํํ๋ ๊ฒ ๊น๋ค๋ก์ ๋ค. (์ฌ์ค Persistence Layer์ ์๋ ์ํฐํฐ๊ฐ ์ฌ๋ฌ ๊ณณ์์ ๋๋ฉ์ธ ๊ฐ์ฒด๋ก ์ฐ์ด๋ ๊ฒ๋ถํฐ๊ฐ ๊ต์ฅํ ์ด์ํ์ง๋ง, ๋ ๊ฑฐ์๋ผ๋ ๊ฒ ์๊ฐ์ฒ๋ผ ์ฝ๊ฒ ๋ฆฌํฉํฐ๋ง์ด ๋๋ ๋ถ๋ถ์ด ์๋์๊ธฐ์ ์ฐ์ ํด๋น ๋ถ๋ถ์ ๋ํด์๋ ๋ค์ ๊ธฐํ์ ๊ณ ์ณ๋๊ฐ๊ณ ์ ํ๋ค.)
์๋ฌดํผ, 2๊ฐ์ง ํฌ์ธํธ๋ฅผ ๋ฐํ์ผ๋ก ์๋์ ๊ฐ์ด ๋ฆฌํฉํฐ๋ง์ ์งํํ๊ณ ์ ํ๋ค.
@Entity
class Student(
val name: String,
val age: Int,
group: StudentGroup
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L
@ManyToOne
@JoinColumn(name = "group_id", insertable = false, updatable = false)
val group: StudentGroup = group
@Embedded
val address: Address = Address.init()
}
@Embeddable
class Address protected constructor() {
@Column
val city: String? = null
@Column
val county: String? = null
@Column
val district: String? = null
companion object {
fun init(): Address {
return Address()
}
}
}
๋จ์ ์ฝ๋์์ผ๋ก๋ ์ ๋ง ๊ฐ๋จํ๋ค. ์ํฐํฐ๋ฅผ ์ฐธ์กฐํ๊ณ ์๋ ๊ฒฝ์ฐ ๊ฐ์ ์ฐธ์กฐ๋ฅผ ์ํ ๋
ผ๋ฆฌ์ FK ๊ฐ์ ์ ๊ฑฐํ๊ณ , '์ฃผ์'๋ผ๋ ๋๋ฉ์ธ์ ๋ง์ถฐ Address๋ผ๋ ์๋ก์ด ํด๋์ค๋ฅผ ์ ์ํ์ฌ @Embedded, @Embeddable๋ฅผ ํตํด ๋ฆฌํฉํฐ๋ง์ ์งํํ์๋ค.
์ฒ์์๋ ๋น์ฆ๋์ค ๋ก์ง์ ์์ ์์ด, ๋จ์ํ ํด๋์ค๋ฅผ ์ถ๊ฐํ ๋ถ๋ถ์ด๊ธฐ ๋๋ฌธ์ ์๋ฌด ๋ฌธ์ ๊ฐ ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์๋ค.
๊ทธ๋ฌ๋, ์ค์ ๋ก๋ ๋งค์ฐ ํฐ 2๊ฐ์ง์ ๋ฌธ์ ์ ์ด ์กด์ฌํ์๋๋ฐ, ์ด๋ ํ
์คํธ ์ผ์ด์ค๋ก ํ ๋ฒ ์ดํด๋ณด์.
๐ฑ Case 1 - JPA ์ฌ์ฉ ์ ์ต์ ์ ์ ๋๋ก ๋ณด์
๋จผ์ , ์ ๋ง ๊ฐ๋จํ๊ฒ ์ ์ฅ ๋ฐ ์กฐํํ๋ ์๋น์ค ์ฝ๋๊ฐ ์๋์ ๊ฐ์ด ์กด์ฌํ๋ค๊ณ ์๊ฐํด๋ณด์.
@Service
class StudentService(
private val studentRepository: StudentRepository,
private val groupRepository: StudentGroupRepository
) {
fun saveGroup(name: String) {
val group = StudentGroup(name)
groupRepository.save(group)
}
fun saveStudent(group: StudentGroup, name: String, age: Int): Student {
val student = Student(name, age, group)
return studentRepository.save(student)
}
}
๊ทธ๋ฆฌ๊ณ , ๊ทธ๋ฃน์ ์ ์ฅํ๊ณ ํด๋น ๊ทธ๋ฃน์ ๋ํด ํ์์ ์ ์ฅํ๋ ์ฝ๋๋ฅผ ํ ์คํธ ์ฝ๋๋ก ์์ฑํด๋ณด์.
@SpringBootTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ActiveProfiles("test")
class StudentServiceTest(
private val studentService: StudentService
) {
@Test
fun test() {
val group = studentService.saveGroup("๊ทธ๋ฃน")
studentService.saveStudent(group, "ํ์", 20)
}
}
์์ ํ ์คํธ ๊ฒฐ๊ณผ๋ ์ด๋ป๊ฒ ๋์ฌ๊น? ์ธ๋ป ๋ณด๋ฉด ์ฑ๊ณตํ ๊ฒ ๊ฐ๋ค.
ํ์ง๋ง, ์ค์ ๋ก๋ ์์ ๊ฐ์ด Field 'group_id' doesn't have a default value ๋ผ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
ํ์ฌ ์ฌํํ ๋๋ ์ ์ฌ์ง์ฒ๋ผ ์ด๋ค ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋์ง ์ค๋ฅ ๋ฉ์์ง๋ก ํจ๊ป ๋์์ง๋ง, ๋น์์๋ default value๊ฐ ์๋ค๋ ์ค๋ฅ๋ง ๋ฐ์ํ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ธ์งํ์ง ๋ชปํ๋ค. (๋ํ, ์ ํ
์คํธ ์ฝ๋์ ๊ฒฝ์ฐ ๋น์ฆ๋์ค ๋ก์ง์ด ์๊ธฐ ๋๋ฌธ์ ๋น์ฐํ ์ด๋ฐ ํ๋ฆ์ด ์ ๋ณด์ด์ง๋ง, ๋ด๊ฐ ๋ถ๋ชํ๋ ์ํฉ์์๋ ๊ทธ ์ฌ์ด์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋๋ฌด ๋ง์์ ํ์
ํ๊ธฐ๊ฐ ์ด๋ ค์ ๋ค.)
๊ทธ๋์ ๋๋ ์ฒ์์๋ student๋ฅผ ์ ์ฅํ ๋ student group์ด ์์ํ ๋์ง ์์ ๊ฐ์ฒด๊ฐ ๋์ด์์ (ํน์ ๊ฐ์ฒด๊ฐ ์์ null๋ก ๋์ด์์) group id๊ฐ null๋ก ๋์ค๋ ๊ฒ์ธ์ง ๊ณ ๋ฏผํ์๋ค. ๊ทธ๋์ ๋๋ฒ๊น
์ ํตํด student๊ฐ ์ ์ฅ๋๋ ์์ ์ group์ ์ฒดํฌํ์๋ค.
ํ์ง๋ง, ์ฌ์ง์ ๋ณด๋ฉด ๋น์ฐํ๊ฒ๋ group ์ ๋ณด์ id ๊ฐ์ด ์ ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ญ๊ฐ ๋ฌธ์ ์์๊น ํ์ฐธ์ ๊ณ ๋ฏผํ๋ค๊ฐ, ์ ์ธ๋์ด ์๋ Entity๋ฅผ ์ฃผ์ ๊น๊ฒ ์ดํด๋ณด๋ ๋ฐ๋ก ์์์ฑ ์ ์์๋ค.
@JoinColumn(name = "group_id", insertable = false, updatable = false)
๋ฐ๋ก, insertable = false ์ต์
์ผ๋ก ์ธํด ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด์๋ค.
์ ํํ๊ฒ ๋งํ๋ฉด, ์คํค๋ง ์์ผ๋ก๋ group_id๊ฐ NOT NULL๋ก ์ง์ ๋์ด ์๋ ์ํ์์, insertable = false๋ก ์ธํด insert ์์ ํด๋น ์ปฌ๋ผ์ ์ ์ธํ๊ณ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ ๋ด๊ฐ ์ด๋ํ๋ ์ค์๋ ๋ค์๊ณผ ๊ฐ๋ค๊ณ ์๊ฐํ๋ค.
1. ๊ฐ๋ฐ์๊ฐ ์์ฑํ Entity๊ฐ ํ ์ด๋ธ ํ์๊ณผ ๊ทธ๋๋ก ์ผ์นํ ๊ฒ์ด๋ผ ์๊ฐํ๊ณ ํ ์ด๋ธ ์คํค๋ง๋ฅผ ์ฃผ์๊น๊ฒ ์ดํด๋ณด์ง ์์ ์
2. ๊ธฐ์กด ๋ ๊ฑฐ์ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ์ฎ๊ฒจ์ผ ์ ๋์ํ๋ค๊ณ ์๊ฐํ๊ณ , ์ต์ ์ ๋ํด ์ฃผ์ ๊น๊ฒ ์ดํด๋ณด์ง ์์ ์ .
1๋ฒ์ ๊ฒฝ์ฐ, ๋ค๋ฅด๊ฒ ์๊ฐํ๋ฉด Persistence Layer์ธ Entity๊ฐ DB์ ํ์๊ณผ ๋ค๋ฅด๊ฒ ์ ์ง๋๋ฉด์ (์ฌ๋ด์์๋ flyway ๊ฐ์ ๋ง์ด๊ทธ๋ ์ด์
ํด์ ์ฌ์ฉํ์ง ์๋๋ค.) ๋ฐ์๋ ์ธ์ง ์ค๋ฅ์๋ค. ํ๋๊ฐ ๋น์ฐํ nullable ํ๊ฒ ์ ์ธ๋์ด ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋ค.
2๋ฒ์ ๊ฒฝ์ฐ, ์ฌ์ค ๊ฐ์ธ์ ์ผ๋ก ๋ฆฌํฉํฐ๋ง์ ์งํํ๋ฉด์ ์กฐ๊ธ๋ง ์์ ํด๋ ์์๊ณผ ๋ค๋ฅด๊ฒ ๋์ํ๋ ์ ์ด ์ฌ๋ฌ ๋ฒ ์๋ค ๋ณด๋ '์ต๋ํ ๊ธฐ์กด ์ฝ๋๋ฅผ ์ ์งํด์ผ๊ฒ ๋ค!'๋ผ๋ ๋ง์ ๊ฐ์ง์์ ํ๋ ํ๋์ด์๋ค. ์ฃผ์๊น๊ฒ ๋ดค๋๋ผ๋ฉด ์ฒ์๋ถํฐ ์ด์ํ ์ ์ ์์์ฑ ์ ์์์ ํ
๋ฐ ๋ถ๋๋ฌ์ธ ์ ๋๋ก ๋ช
ํํ ๋ด ์ค์ ๊ทธ ์์ฒด์๋ค.
๊ทธ๋ ๋ค๋ฉด, ์ง๊ธ๊น์ง๋ ์ด๋ป๊ฒ insert๊ฐ ๋์๋ ๊ฒ์ผ๊น?
@ManyToOne
@JoinColumn(name = "group_id", insertable = false, updatable = false)
val group: StudentGroup = group
@Column(name = "group_id")
val groupId: Long = group.id
๋ด๊ฐ ๋ฆฌํฉํฐ๋ง์ ํ๋ ค๊ณ ํ๋ ๊ทธ groupId๋ผ๋ ํ๋๋ฅผ ํตํด์ insert๊ฐ ์ ์์ ์ผ๋ก ์งํ์ด ๋๊ณ ์๋ ์ํ์๋ค.
์๋ง ์ฝ๋๋ฅผ ์ฒ์ ์์ฑํ์ ๋ถ์ ์๋๋ก๋ (ํ์คํ์ง๋ ์์ง๋ง) Spring data JPA๋ฅผ ์ฌ์ฉํ ๋ ๋ฉ์๋๋ช
์ ํตํด select๋ฅผ ๋ง์ด ํ๋๋ฐ, group์ด๋ผ๋ ๋๋ฉ์ธ ๋์ ์ id๋ฅผ ํตํด์ ๋ฐ๋ก ๊ฐ์ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ํ๊ธฐ ์ํด ๋ฐ๋ก ๋ถ๋ฆฌํ์ ๊ฒ ์๋๊น ์ถ๋ค.
Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: Column 'group_id' is duplicated in mapping for entity
๋ํ, ๋ ๋ค insert๊ฐ ๊ฐ๋ฅํ ์ํ๋ผ๋ฉด ์์ ๊ฐ์ด ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ์ํฐํฐ์ ๋ํด์๋ insertable=false๋ก ๋ง์๋์ ๊ฑธ๋ก ์ถ์ธก๋๋ค.
์ด์ ์ ์์ฑํ๋ ์ํฐํฐ๋ฅผ ๋ค์ ๋กค๋ฐฑํ ๋ค์ ํ
์คํธ ์ฝ๋๋ฅผ ๋๋ ค๋ณด์.
๋ฐ์ธ๋ฉ ํ๋ผ๋ฏธํฐ ์ต์
์ ์ผ๊ณ ํ์ธํด๋ณด๋ฉด, group_id์ ์์ด๋ ๊ฐ์ด ์ ๋๋ก ๋ค์ด๊ฐ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์ํฐํฐ ์ง์ ์ฐธ์กฐ ์ ์ง + insertable = false๋ฅผ ์ฌ์ฉํ๊ฑฐ๋, ๊ธฐ์กด ํ์์ ์ ์งํ๋ฉด ๋๋ค.
๐ฑ Case 2 - ์ ์ ์๋ ํ์ด๋ฒ๋ค์ดํธ
์ฌ์ค ์ ๋ฌธ์ ์ ๊ฒฝ์ฐ ๋น์ฆ๋์ค ๋ก์ง ์ํ ํ ๊ธ๋ฐฉ ์กํ ์ ์๋ ์ผ์ด์ค์์ง๋ง, ์ด๋ฒ ์ผ์ด์ค๋ ์กฐ๊ธ ์ ๊ธฐํ ๊ฒฝ์ฐ์ด๋ค.
fun findStudent(id: Long): Student {
return studentRepository.findById(id).orElseThrow()
}
์์ ๊ฐ์ด ์กฐํ๋ฅผ ์ํ ๋น์ฆ๋์ค๊ฐ ์ถ๊ฐ๋์๊ณ , ์ค์ง์ ์ผ๋ก ์๋์ ๊ฐ์ ๋ก์ง์ด์๋ค๊ณ ์๊ฐํด๋ณด์.
@Test
fun test2() {
val group = studentService.saveGroup("๊ทธ๋ฃน")
val student = studentService.saveStudent(group, "ํ์", 20)
val findStudent = studentService.findStudent(student.id)
// ํ์์ address ์ ๋ณด๋ฅผ ๋ค๋ฅธ api๋ก ์ ๋ฌํ๋ค๊ณ ๊ฐ์
callExternalApi(findStudent.address)
}
fun callExternalApi(address: Address) {
// ๋์ถฉ address์ ๊ฐ ์์์ ๋ํด์ ์ ๊ทผํ์ฌ ํธ์ถํ๋ค๊ณ ๊ฐ์
println("address.city = ${address.city}")
println("address.county = ${address.county}")
println("address.district = ${address.district}")
}
์ด๋, ์ ์ฝ๋๋ ์ด๋ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊น?
๋น์ ๋ด๊ฐ ์ดํดํ๋ ํ๋ฆ์ผ๋ก๋, address์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ๊ฐ์ผ๋ก Address.init()์ ํตํด ๋น ๊ฐ์ฒด๋ฅผ ํ ๋นํด์ฃผ์๊ธฐ ๋๋ฌธ์ ์ค์ง์ ์ผ๋ก city, county, district ๊ฐ์ด null์ธ address๊ฐ ํ ๋น์ด ๋์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์๋ค.
ํ์ง๋ง, ์ค์ ๋ก ์ ์ฝ๋์ ๊ฒฝ์ฐ NPE๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
์ฝํ๋ฆฐ ๊ฐ์ ์ธ์ด์์ NPE๊ฐ ๋ฐ์ํ๋ค๋ ๊ฑด ๊ต์ฅํ ์ฃผ์๊น๊ฒ ์ดํด๋ด์ผ ํ๋ ๋ถ๋ถ์ด๋ผ์, ์ฒ์์๋ ๋งค์ฐ ๋นํฉํ์๋ค.
๋ํ, ๋น์ฆ๋์ค ๋ก์ง์์ ๋ฌด์กฐ๊ฑด ๋ฐ์ํ๋ ๊ฒ ์๋๋ผ ๋ถ๊ธฐ์ ๋ฐ๋ผ์ ๋ฐ์ํ ๋ก์ง์ด์์ด์ ์๋ชปํ๋ฉด ์ธ์งํ์ง ๋ชปํ๊ณ ๋ฐฐํฌ๋ฅผ ๋๊ฐ์ ์๋ ์๋ ๋ถ๋ถ์ด๋ผ์ ํ ํธ์ผ๋ก๋ ์ง๊ธ ์กํ์ ๋คํ์ด๋ผ๊ณ ์๊ฐํ์๋ค... ใ
ใ
์๋ฌดํผ, ์ค์ ๋ก ๋๋ฒ๊น
์ ํตํด ํ์ธํด๋ณด์๋ ์์ ๊ฐ์ด address์ null์ด ๋ค์ด๊ฐ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
Address ๊ฐ์ฒด ์์ฒด๋ฅผ non-nullableํ ํ์
์ผ๋ก ์ค์ ํ์๋๋ฐ ์ด๋ป๊ฒ NPE๊ฐ ๋ฐ์ํ ๊ฒ์ผ๊น?
์ฒ์์๋ ๋์ปดํ์ผ์ ํตํด, ๋ญ๊ฐ JPA์ ๋ด๊ฐ ๋ชจ๋ฅด๋ ๊ธฐ๋ฅ์ผ๋ก ์ธํด nullableํ ํ์
์ผ๋ก ์ ์ธ๋๋ค๊ณ ์๊ฐํ์๋ค.
ํ์ง๋ง, ํ๋์ getter์ ๋ํด ๋ชจ๋ @NotNull ์ด๋
ธํ
์ด์
์ด ๋ถ์ด์๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
์ฒ์์๋ ๋ด๊ฐ ์ฝํ๋ฆฐ์ ์๋ชป ์๊ณ ์๋ ๊ฑด๊ฐ ์ถ์ด์ ๋ง ๊ตฌ๊ธ๋ง์ ํ๋ค๊ฐ ์๋์ ๊ฐ์ ์ด์๋ฅผ ๋ฐ๊ฒฌํ์๋ค.
When all of the values in an @Embedded object are NULL, Hibernate sets the field in the parent object to null.
This can lead to NullPointerExceptions if not handled correctly.
์์ฝ: Embedded ๊ฐ์ฒด์ ๋ชจ๋ ํ๋๊ฐ null์ด๋ฉด ํด๋น ๊ฐ์ฒด๋ null๋ก ํ์ด๋ฒ๋ค์ดํธ๊ฐ ์ธํ
์ ํ๋ค.
๊ฐ๋งํ ์๊ฐํด๋ณด๋ฉด JPA์ ๊ฒฝ์ฐ ๋ฆฌํ๋์
์ ํตํด ๊ฐ์ฒด๋ฅผ ์ธํ
ํ๋๊น, NotNull๋ก ์ธํ
ํ๋๋ผ๋ ์ถฉ๋ถํ null๋ก ์ธํ
์ด ๊ฐ๋ฅํ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. (์ฌ์ค ์ด๋ถ๋ถ์ ์ง์ ์ฌํํด๋ณด๋ ค๊ณ ํ๋๋ฐ ์๊ฐ๋ณด๋ค ๊ณต์๊ฐ ๋ ๋ค์ด๊ฐ์ ์ฐ์ ํจ์ค...)
์๋ฌดํผ, ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด hibernate์ ๋ฆด๋ฆฌ์ฆ ๋
ธํธ๋ฅผ ๋ดค๋๋ฐ ํด๊ฒฐ์ฑ
์ด ์์๋ค! (5.1 version)
spring.jpa.properties.hibernate.create_empty_composites.enabled=true
๋ฐ๋ก, ์์ ์ต์
์ true๋ก ์ง์ ํด์ฃผ๋ฉด ๋๋ ๊ฒ์ด์๋ค.
๊ถ๊ธํด์ ํ์ด๋ฒ๋ค์ดํธ ์ชฝ์ ์ด์ง ์ฐพ์๋ดค๋๋ฐ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํ๋์ด ์๋ค.
public ComponentMetamodel(Component component, MetadataBuildingOptions metadataBuildingOptions) {
...
final ConfigurationService cs = component.getMetadata().getMetadataBuildingOptions().getServiceRegistry()
.getService(ConfigurationService.class);
this.createEmptyCompositesEnabled = ConfigurationHelper.getBoolean(
Environment.CREATE_EMPTY_COMPOSITES_ENABLED,
cs.getSettings(),
false
);
}
public ComponentType(TypeFactory.TypeScope typeScope, ComponentMetamodel metamodel) {
...
this.createEmptyCompositesEnabled = metamodel.isCreateEmptyCompositesEnabled();
}
์ด๋ ๊ฒ create_empty_composites.enabled์ ๋ํ ์ต์ ์ ๋ณด๋ฅผ ํตํด์ ํ๋๊ทธ๋ฅผ ์ง์ ํด์ฃผ๊ณ ...!
// ๊ฐ์ฒด์ ๊ฐ ๋น๊ต ๋ก์ง ์ค ์ผ๋ถ
// null value and empty component are considered equivalent
Object[] xvalues = getPropertyValues( x, entityMode );
Object[] yvalues = getPropertyValues( y, entityMode );
์์ ๋ชจ๋ฅด๊ฒ ์ง๋ง null value and empty component are considered equivalent ๋ผ๋ ์ฃผ์๋ ์์๋ค.
(๋ด๋ถ์ ์ผ๋ก null ๊ณผ ๋น ๊ฐ์ฒด๊ฐ ๋์ผํ๋ค๊ณ ํ๋จํ๊ธฐ ์ํจ์ธ ๊ฒ ๊ฐ๋ค)
// ํ๋กํผํฐ ๊ฐ์ ธ์ฌ ๋
public Object getPropertyValue(Object component, int i)throws HibernateException {
if (component == null) {
component = new Object[propertySpan];
}
...
}
// resolve ํ ๋
@Override
public Object resolve(Object value, SessionImplementor session, Object owner) throws HibernateException {
if ( value != null ) {
Object result = instantiate( owner, session );
Object[] values = (Object[]) value;
Object[] resolvedValues = new Object[values.length]; //only really need new array during semiresolve!
for ( int i = 0; i < values.length; i++ ) {
resolvedValues[i] = propertyTypes[i].resolve( values[i], session, owner );
}
setPropertyValues( result, resolvedValues, entityMode );
return result;
}
// ์ฌ๊ธฐ์ ์๊น ์ต์
์ด ํ์ฑํ ๋์ด ์๋ ๊ฒฝ์ฐ ์ ์์ ์ผ๋ก ์ด๊ธฐํํ๋๋ก!
else if ( isCreateEmptyCompositesEnabled() ) {
return instantiate( owner, session );
}
else {
return null;
}
}
๊ทธ๋ฆฌ๊ณ ๋น ๊ฐ์ฒด์ ๋ํด์ ์ด๊ธฐํ๊ฐ ๊ฐ๋ฅํ๋๋ก ๋์ด ์๋ค. (else if ๋ถ๊ธฐ)
์๋์์ผ๋ฉด else ๋ฌธ์ผ๋ก ์ธํด์ null์ด ๋ฐํ๋์์ ํ
๋ฐ, ์ ๋ถ๊ธฐ ๋๋ถ์ ๋น ๊ฐ์ฒด๋ก ์ด๊ธฐํ๊ฐ ๊ฐ๋ฅํ๊ฒ ๋ ๊ฒ์ด๋ค.
์๋ฌดํผ, ์์ ์ต์
์ ์ ์ฉํด๋ณด๊ณ ๋๋ ค๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ๋น ๊ฐ์ฒด๊ฐ ์ ํ ๋น๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค!
๐ฑ ๊ฐ๋จ ํ๊ณ
์ฌ์ค ์ฃผ์ ๋๋ฉ์ธ์ธ ๋งํผ ์ ์คํ๊ฒ ๋ฆฌํฉํฐ๋ง์ ์งํํ์ด์ผ ํ๋๋ฐ, ๊ฐ๋จํ ์์ ์ด๋ผ ํฐ ์ํฅ์ด ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ ๊ฒ์ด ๋ฌธ์ ์๋ ๊ฒ ๊ฐ๋ค.
1. ๋ฌด์กฐ๊ฑด ์ํฐํฐ์ ์์ฑ๋ ๊ฒ๋ง ๋ณด๊ณ ์์กดํ์ง ๋ง์.
์ง๊ธ๊น์ง ๊ฐ์ธ ํ๋ก์ ํธ๋ฅผ ํ์ ๋๋ ํ
์ด๋ธ ์ธํ
๋ถํฐ ๋ด๊ฐ ์งํํ๋ค ๋ณด๋๊น ์์ฐ์ค๋ฝ๊ฒ JPA ์ํฐํฐ๊ฐ ํ
์ด๋ธ์ ํ์์ ๊ทธ๋๋ก ๋ฐ๋ผ๊ฐ๋ค๊ณ ์๊ฐํ๋ ๊ฒ ๊ฐ๋ค. ํ์ง๋ง, ํ์
์์๋ ์ด๋ฏธ ํ
์ด๋ธ์ด ์ธํ
๋์ด ์๊ณ , ํ
์ด๋ธ์ ์คํค๋ง๊ฐ ๋ณํจ์๋ ๋ถ๊ตฌํ๊ณ ์ํฐํฐ๊ฐ ์
๋ฐ์ดํธ ๋์ง ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. ์์ผ๋ก๋ ํ
์ด๋ธ ์คํค๋ง๋ถํฐ ๋จผ์ ์ฒดํฌ๋ฅผ ํด๋ด์ผ๊ฒ ๋ค.
2. ๋ ๊ฑฐ์๋ผ๊ณ ๋ฌด์กฐ๊ฑด ๋ฐ๋ผ๊ฐ์ง ๋ง๊ธฐ
์์ ์ ์คํ์๊ณ ํ์ง๋ง, ๋ฌด์๋ณด๋ค ๋ ๊ฑฐ์ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ๋ฏฟ๊ณ ๋ฐ๋ผ๊ฐ๋ ๊ฑด ๋ค๋ฅธ ๋ ๊ฑฐ์๋ฅผ ๋ ๋ณ๋ ํ๋์ธ ๊ฒ ๊ฐ๋ค.
์ ์คํ๊ฒ ์ํฅ ๋ฒ์๋ฅผ ์ ํ์
ํด์ ์กฐ๊ธ์ฉ ๋ฆฌํฉํฐ๋ง์ ์งํํ๋ ๊ฒ ๊ฐ์ฅ ์ค์ํ ๊ฒ ๊ฐ๋ค.
์ฌ์ค ์ด๋ฒ ๋ฆฌํฉํฐ๋ง์ ๊ท๋ชจ๊ฐ ์ปค์ ๋ค์ ๋กค๋ฐฑํ์ง๋ง... ๐ฅฒ 1์์ ๋ค์ ํ ๋ฒ ์๋ํด๋ณผ ์์ ์ด๋ค.
3. JPA์ ๋ฌ๋์ปค๋ธ ๊ณ ๋ฏผํด๋ณด๊ธฐ
์ฌ์ค ์ฐ๋ฆฌ ํ์ ๊ฒฝ์ฐ JPA์ ๊ธฐ๋ฅ์ ์ ๋๋ก ํ์ฉํ์ง ์๋๋ค. ์ ๋ง CRUD๋ฅผ ํธํ๊ฒ ํด์ฃผ๋ ๋๊ตฌ ๊ทธ ์ด์, ๊ทธ ์ดํ๋ ์๋๊ฒ ์ฌ์ฉํ๋ ๋๋?
์์ ์๋ JPA๊ฐ ๋ฌด์กฐ๊ฑด ์ข๋ค๊ณ ์๊ฐํ๋๋ฐ, ํ์ฌ๋ฅผ ์
์ฌํ๊ณ ๋ ๊ฐ์ฅ ํฐ ์๊ฐ์ ๋ชจ๋๊ฐ ์๊ฐํ๋ ์งํฅ์ ๊ณผ ๋ฌ๋ ์ปค๋ธ๊ฐ ์๋ก์ด ๊ธฐ์ ์ ๋์
ํ๊ณ ์ด๋ ํ ๊ฒ์ ์ฌ์ฉํ ๋ ๊ฐ์ฅ ์ค์ํ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. ์ฌ์ฉํ ๊ฑฐ๋ฉด ์ ๋๋ก ์์๋ณด๊ณ , ํ์ ๋ชจ๋๊ฐ ํด๋น ์ง์์ ๋ํด ์๋ฒฝํ๊ฒ ์ฑํฌ๊ฐ ๋์ด ์์ ๋ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ๋ค.
JPA์ ๋ฌ๋์ปค๋ธ๋ฅผ ์๊ฐํด๋ณด์์ ๋, @ManyToOne ๊ฐ์ JPA ์ข
์์ ์ธ ์ด๋
ธํ
์ด์
์ ๋ง์ด ์ฌ์ฉํด์ผ ํ๋๊ฐ? ๋ผ๋ ์๋ฌธ์ ๋ ๋ค๊ณ ... ์๋ง ๋ค์ ๋ฆฌํฉํฐ๋ง ๋๋ @ManyToOne ๊ฐ์ ์ด๋
ธํ
์ด์
์ ์ ๊ฑฐํ๋ ๋ฐฉํฅ๋ ํ ๋ฒ ๊ณ ๋ คํด๋ณผ ๊ฒ ๊ฐ๋ค.
์ด๋ฒ @Embedded ์ผ์ด์ค๋ ์ฌ์ค ํ ๋ด์์ ๊ฐ์ฅ ์ต์ํ ์ฌ๋์ด ๋์์ด์, ๋ค์ ์ฌ์ฉํ์ง ์๋ ๋ฐฉํฅ์ผ๋ก ๋์๊ฐ๋ ๊ฑธ ๊ณ ๋ฏผ ์ค์ด๋ค. (๋ชจ๋๊ฐ ํธํ๊ฒ ์ฌ์ฉํ๋ ๊ฒ ๊ฐ์ฅ ์ค์ํ๋๊น)
์ค๋๋ง์ ๋ธ๋ก๊ทธ ๊ธ์ ์์ฑํ๋ค ๋ณด๋ ์๊ฐ๋ณด๋ค ์๊ฐ๋ ๋ง์ด ์ฐ์์ง๋ง, ๋ฐํ๊น์ง ํ๋ ์ฃผ์ (...) ์๋ ๊ฒ ๋งํผ ๊ฐ์ธ์ ์ผ๋ก ์ ๋ฆฌํด๋ณด๊ณ ์ถ์๋ค. ๋ด๋
๋ถํฐ๋ ๋ค์ ์กฐ๊ธ์ฉ ๋ธ๋ก๊ทธ ๊ธ๋ ์ด์ฌํ ์์ฑํด์ผ๊ฒ ๋ค... ํ์ดํ
!