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 ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์–ด์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
 

์ฒ˜์Œ ์˜ค๋ฅ˜๋ฅผ ๋ดค์„ ๋•Œ ๋‚˜์˜ ์‹ฌ์ •...

 
์ฒ˜์Œ์—๋Š” ๋‚ด๊ฐ€ ์ฝ”ํ‹€๋ฆฐ์„ ์ž˜๋ชป ์•Œ๊ณ  ์žˆ๋Š” ๊ฑด๊ฐ€ ์‹ถ์–ด์„œ ๋ง‰ ๊ตฌ๊ธ€๋ง์„ ํ•˜๋‹ค๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์€ ์ด์Šˆ๋ฅผ ๋ฐœ๊ฒฌํ•˜์˜€๋‹ค.

[HHH-7610] - Hibernate JIRA

hibernate.atlassian.net

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 ์ผ€์ด์Šค๋„ ์‚ฌ์‹ค ํŒ€ ๋‚ด์—์„œ ๊ฐ€์žฅ ์ต์ˆ™ํ•œ ์‚ฌ๋žŒ์ด ๋‚˜์˜€์–ด์„œ, ๋‹ค์‹œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๋‚˜์•„๊ฐ€๋Š” ๊ฑธ ๊ณ ๋ฏผ ์ค‘์ด๋‹ค. (๋ชจ๋‘๊ฐ€ ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ์ค‘์š”ํ•˜๋‹ˆ๊นŒ)

์˜ค๋žœ๋งŒ์— ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ž‘์„ฑํ•˜๋‹ค ๋ณด๋‹ˆ ์ƒ๊ฐ๋ณด๋‹ค ์‹œ๊ฐ„๋„ ๋งŽ์ด ์“ฐ์˜€์ง€๋งŒ, ๋ฐœํ‘œ๊นŒ์ง€ ํ–ˆ๋˜ ์ฃผ์ œ(...) ์˜€๋˜ ๊ฒƒ ๋งŒํผ ๊ฐœ์ธ์ ์œผ๋กœ ์ •๋ฆฌํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋‹ค. ๋‚ด๋…„๋ถ€ํ„ฐ๋Š” ๋‹ค์‹œ ์กฐ๊ธˆ์”ฉ ๋ธ”๋กœ๊ทธ ๊ธ€๋„ ์—ด์‹ฌํžˆ ์ž‘์„ฑํ•ด์•ผ๊ฒ ๋‹ค... ํŒŒ์ดํŒ…!

Comments