4 λΆ„ μ†Œμš”

JPAλž€?

ν˜„λŒ€ μ›Ή μ—ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ κ΄€κ³„ν˜• λ°μ΄ν„°λ² μ΄μŠ€(RDB)λŠ” μ ˆλŒ€ 빠질 수 μ—†λŠ” μš”μ†Œμ΄λ‹€.
Oracle, MySQL, MsSQL 등을 쓰지 μ•ŠλŠ” μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ 거의 μ—†μ„μ •λ„λ‘œβ€¦
κ·ΈλŸ¬λ‹€λ³΄λ‹ˆ 객체λ₯Ό κ΄€κ³„ν˜• λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ κ΄€λ¦¬ν•˜λŠ” 것이 맀우 μ€‘μš”ν•˜λ‹€.

κ²°κ΅­ ν˜„μ—… ν”„λ‘œμ νŠΈμ˜ λŒ€λΆ€λΆ„μ΄ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ½”λ“œλ³΄λ‹€ SQL이 λ§Žμ•„μ§€κ²Œ λ˜μ—ˆλ‹€.
μ΄μœ λŠ” RDBκ°€ SQL만 인식할 수 있기 λ•Œλ¬Έμ΄λ‹€.
κ·Έλž˜μ„œ 기본적인 CRUD SQL을 맀번 μƒμ„±ν•΄μ•Όν•œλ‹€.

이런 SQL을 λ§Œλ“œλŠ” λ‹¨μˆœ 방볡 μž‘μ—…λ„ μžˆμ§€λ§Œ, νŒ¨λŸ¬λ‹€μž„ λΆˆμΌμΉ˜λΌλŠ” 문제점이 μ‘΄μž¬ν•œλ‹€.
RDBλŠ” μ–΄λ–»κ²Œ 데이터λ₯Ό μ €μž₯할지에 초점이 λ§žμΆ°μ§„ κΈ°μˆ μ΄λ‹€.
λ°˜λŒ€λ‘œ 객체지ν–₯ ν”„λ‘œκ·Έλž˜λ° μ–Έμ–΄λŠ” λ©”μ‹œμ§€λ₯Ό 기반으둜 κΈ°λŠ₯κ³Ό 속성을 ν•œ κ³³μ—μ„œ κ΄€λ¦¬ν•˜λŠ” κΈ°μˆ μ΄λ‹€.

이렇듯, RDB와 객체지ν–₯ ν”„λ‘œκ·Έλž˜λ° μ–Έμ–΄κ°„μ˜ νŒ¨λŸ¬λ‹€μž„ λΆˆμΌμΉ˜κ°€ λ°œμƒν•œλ‹€.
객체지ν–₯ ν”„λ‘œκ·Έλž˜λ°μ—μ„œ λΆ€λͺ¨κ°€ λ˜λŠ” 객체λ₯Ό κ°€μ Έμ˜€λ €λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?

User user = findUser();
Group group = user.getGroup();

μœ„μ™€ 같은 μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ©΄ λ˜λŠ”λ°, 여기에 λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μΆ”κ°€ν•˜λ©΄ μ•„λž˜μ™€ κ°™λ‹€.

User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroup());

μœ„μ™€ 같이 User, Group 각각 λ”°λ‘œ μ‘°νšŒν•˜κ²Œ λœλ‹€.
User와 Group이 μ–΄λ–€ 관계인지 μ•Œ 수 μžˆμ„κΉŒ?
상속, 1:N λ“±μ˜ λ‹€μ–‘ν•œ 객체 λͺ¨λΈλ§μ„ λ°μ΄ν„°λ² μ΄μŠ€λ‘œ κ΅¬ν˜„ν•  수 μ—†λ‹€.

μ΄λŸ¬ν•œ λ¬Έμ œμ μ„ ν•΄κ²°ν•˜κΈ° μœ„ν•΄ λ“±μž₯ν•œ 것이 λ°”λ‘œ JPA이닀.

κ·Έλž˜μ„œ JPAλž€?

μ„œλ‘œ 지ν–₯ν•˜λŠ” λ°”κ°€ λ‹€λ₯Έ 2개의 μ˜μ—­μ„ μ€‘κ°„μ—μ„œ νŒ¨λŸ¬λ‹€μž„ 일치λ₯Ό μ‹œμΌœμ£ΌκΈ° μœ„ν•œ κΈ°μˆ μ΄λ‹€.
즉, κ°œλ°œμžλŠ” 객체지ν–₯적으둜 ν”„λ‘œκ·Έλž˜λ°μ„ ν•˜κ³ , JPAκ°€ 이λ₯Ό RDB에 맞게 SQL을 λŒ€μ‹  μƒμ„±ν•΄μ„œ μ‹€ν–‰ν•œλ‹€.
이둜써 항상 κ°œλ°œμžλŠ” 객체지ν–₯적으둜 μ½”λ“œλ₯Ό μž‘μ„±ν•˜μ—¬ SQL에 쒅속적인 κ°œλ°œμ„ ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

Spring Data JPA

JPAλŠ” μΈν„°νŽ˜μ΄μŠ€λ‘œμ„œ, μžλ°” ν‘œμ€€λͺ…μ„Έμ„œμ΄λ‹€.
μΈν„°νŽ˜μ΄μŠ€μΈ JPAλ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” κ΅¬ν˜„μ²΄μΈ Hibernate, EclipseLink 등이 μžˆλ‹€.
ν•˜μ§€λ§Œ, Springμ—μ„œ JPAλ₯Ό μ‚¬μš©ν•  λ•ŒλŠ” 이 κ΅¬ν˜„μ²΄λ“€μ„ 직접 λ‹€λ£¨μ§€λŠ” μ•ŠλŠ”λ‹€.

κ΅¬ν˜„μ²΄λ“€μ„ μ’€ 더 μ‰½κ²Œ μ‚¬μš©ν•˜κ³ μž μΆ”μƒν™”μ‹œν‚¨ Spring Data JPAλΌλŠ” λͺ¨λ“ˆμ„ μ‚¬μš©ν•œλ‹€!
μ΄λ“€μ˜ κ΄€κ³„λŠ” μ•„λž˜μ™€ κ°™λ‹€.

Spring Data JPA -> Hibernate -> JPA

μž₯점1: κ΅¬ν˜„μ²΄ ꡐ체의 μš©μ΄μ„±

κ΅¬ν˜„μ²΄ ꡐ체의 μš©μ΄μ„±μ΄λž€, Hibernate 외에 λ‹€λ₯Έ κ΅¬ν˜„μ²΄λ‘œ μ‰½κ²Œ κ΅μ²΄ν•˜κΈ° μœ„ν•¨μ΄λ‹€.
μ‰½κ²Œ μƒκ°ν•˜λ©΄ Hibernateκ°€ 수λͺ…을 λ‹€ν•΄ μƒˆλ‘œμš΄ JPA κ΅¬ν˜„μ²΄κ°€ λŒ€μ„Έλ‘œ λ– μ˜€λ₯΄κ²Œ 될 λ•Œ, Spring Data JPAλ₯Ό μ‚¬μš©ν•˜κ³  μžˆλ‹€λ©΄ μ‰½κ²Œ κ°ˆμ•„νƒˆ 수 μžˆλ‹€.

μ‹€μ œλ‘œ μžλ°”μ˜ Redis ν΄λΌμ΄μ–ΈνŠΈκ°€ Jedisμ—μ„œ Lettuce둜 λŒ€μ„Έκ°€ λ„˜μ–΄κ°ˆ λ•Œ, Spring Data Redisλ₯Ό μ‚¬μš©ν•œ μœ μ €λ“€μ€ μ‰½κ²Œ ꡐ체λ₯Ό ν–ˆλ‹€κ³  ν•œλ‹€.

μž₯점2: μ €μž₯μ†Œ ꡐ체의 μš©μ΄μ„±

데이터 νŠΈλž˜ν”½μ΄ λ§Žμ•„μ§ˆ 수둝 κ΄€κ³„ν˜• λ°μ΄ν„°λ² μ΄μŠ€λ‘œλŠ” 감당이 μ•ˆλ  수 μžˆλ‹€.
이 λ•Œ MongoDB둜 ꡐ체가 ν•„μš”ν•˜λ‹€λ©΄ κ°œλ°œμžλŠ” Spring Data JPAμ—μ„œ Spring Data MongoDB둜 μ˜μ‘΄μ„±λ§Œ κ΅μ²΄ν•˜λ©΄ λœλ‹€λŠ” μž₯점이 μžˆλ‹€.

μ‚¬μš©λ²•

쒅속성 μΆ”κ°€

[pom.xml]

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

[build.gradle]

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

상속

[UserRepository]

public interface UserRepository extends JpaRepository<User, Long> {

    // λ‹¨μˆœ interface μƒμ„±ν•©λ‹ˆλ‹€.
    // JpaRepository<>λ₯Ό 상속 -> 기본적인 CRUD λ©”μ†Œλ“œ μžλ™ 생성 (@Repository μΆ”κ°€ ν•„μš” μ—†μŠ΅λ‹ˆλ‹€.)
    // CRUD λ©”μ†Œλ“œ: findById, findAll, save, delete λ“±...
}

public interface UserRepository extends JpaRepository<User, Long>의 μ˜λ―ΈλŠ” Spring Data JPAμ—μ„œ μ œκ³΅ν•˜λŠ” JpaRepository μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν™•μž₯ν•˜μ—¬ User μ—”ν‹°ν‹°(β€˜User’)의 CRUD μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ„ μ–Έν•œλ‹€λŠ” μ˜λ―Έμ΄λ‹€.

UserRepositoryλŠ” JpaRepository<User, Long>을 ν™•μž₯ν•˜κ³  있으며, Generic νƒ€μž…μœΌλ‘œλŠ” User, κΈ°λ³Έ ν‚€μ˜ νƒ€μž…μœΌλ‘œλŠ” β€˜Long’을 μ§€μ •ν•˜κ³  μžˆλ‹€.

μ£Όμš” μ• λ…Έν…Œμ΄μ…˜

@Entity

ν…Œμ΄λΈ”κ³Ό 링크될 ν΄λž˜μŠ€μž„μ„ λ‚˜νƒ€λ‚Έλ‹€.
κΈ°λ³Έκ°’μœΌλ‘œ ν΄λž˜μŠ€μ™€ μΉ΄λ©œμΌ€μ΄μŠ€μ˜ 이름을 μ–Έμ–΄μŠ€μ½”μ–΄ 넀이밍(_)으둜 ν…Œμ΄λΈ” 이름을 λ§€μΉ­ν•œλ‹€.

  • 예: SalesManager.java -> sales_manager table

Entity ν΄λž˜μŠ€μ—λŠ” μ ˆλŒ€ setterλ©”μ†Œλ“œλ₯Ό λ§Œλ“€μ§€ μ•ŠλŠ”λ‹€.
μ΄μœ λŠ” ν•΄λ‹Ή 클래슀의 μΈμŠ€ν„΄μŠ€ 값듀이 μ–Έμ œ μ–΄λ””μ„œ λ³€ν•΄μ•Ό ν•˜λŠ”μ§€ μ½”λ“œμƒμœΌλ‘œ λͺ…ν™•ν•˜κ²Œ ꡬ뢄할 μˆ˜κ°€ μ—†μ–΄, μ°¨ν›„ κΈ°λŠ₯ λ³€κ²½ μ‹œ λ³΅μž‘ν•΄μ§ˆ 수 있기 λ•Œλ¬Έμ΄λ‹€.

λŒ€μ‹ , ν•΄λ‹Ή ν•„λ“œμ˜ κ°’ 변경이 ν•„μš”ν•˜λ©΄ λͺ…ν™•νžˆ κ·Έ λͺ©μ κ³Ό μ˜λ„λ₯Ό λ‚˜νƒ€λ‚Ό 수 μžˆλŠ” λ§€μ†Œλ“œλ₯Ό μΆ”κ°€ν•΄μ•Όλ§Œ ν•œλ‹€.

그러면, μ–΄λ–»κ²Œ λ°μ΄ν„°λ² μ΄μŠ€μ— 값을 μ±„μ›Œ μ‚½μž…ν• κΉŒ? μƒμ„±μž λŒ€μ‹  @Builderλ₯Ό 톡해 μ œκ³΅λ˜λŠ” λΉŒλ” 클래슀λ₯Ό μ‚¬μš©ν•˜λ©΄ λœλ‹€.

@Id

ν•΄λ‹Ή ν…Œμ΄λΈ”μ˜ PK ν•„λ“œλ₯Ό λ‚˜νƒ€λ‚Έλ‹€.

@GeneratedValue

PK의 생성 κ·œμΉ™μ„ λ‚˜νƒ€λ‚Έλ‹€.
μŠ€ν”„λ§λΆ€νŠΈ 2.0μ—μ„œλŠ” GenerationType.IDENTITY μ˜΅μ…˜μ„ μΆ”κ°€ν•΄μ•Όλ§Œ SQLμ—μ„œ PK에 λŒ€ν•΄ auto_incrementκ°€ μ μš©λœλ‹€.

@Column

ν…Œμ΄λΈ”μ˜ μΉΌλŸΌμ„ λ‚˜νƒ€λ‚΄λ©° ꡳ이 μ„ μ–Έν•˜μ§€ μ•Šλ”λΌλ„ ν•΄λ‹Ή 클래슀의 ν•„λ“œλŠ” λͺ¨λ‘ 칼럼이 λœλ‹€.
μ‚¬μš©ν•˜λŠ” μ΄μœ λŠ” κΈ°λ³Έκ°’ 외에 μΆ”κ°€λ‘œ 변경이 ν•„μš”ν•œ μ˜΅μ…˜μ΄ 있으면 μ‚¬μš©ν•œλ‹€.

@Column(length = 500, nullable = false)
private String name;

μœ„μ˜ μ˜ˆμ‹œμ—μ„œλŠ” @Column에 length=500, nullable=false μ˜΅μ…˜μ΄ μ μš©λ˜μ—ˆλ‹€.
length = 500의 μ˜λ―ΈλŠ” name λ³€μˆ˜μ˜ μ΅œλŒ€ 길이가 500으둜 μ œν•œν•œλ‹€λŠ” μ˜λ―Έμ΄λ‹€.
SQLμ—μ„œ varchar(500)으둜 λ³Ό 수 μžˆλ‹€.
이 λ•Œ, UTF-8 인코딩 κΈ°μ€€μœΌλ‘œ ν•œκΈ€μ€ ν•œ κΈ€μžλ₯Ό 3λ°”μ΄νŠΈλ‘œ ν‘œν˜„ν•˜κΈ° λ•Œλ¬Έμ— μ•½ 166μžκ°€ κ°€λŠ₯ν•˜λ‹€.

nullable = falseλŠ” SQLμ—μ„œ not null의 의미둜 보면 λœλ‹€.
즉, ν•΄λ‹Ή 컬럼이 null값을 ν—ˆμš©ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” μ˜λ―Έμ΄λ‹€.

μ΄λ ‡κ²Œ @Column(length = 500, nullable = false)λ₯Ό μ‚¬μš©ν•΄ ν•„λ“œμ— 컬럼 맀핑 정보λ₯Ό μ§€μ •ν•˜λ©΄ JPAκ°€ ν•΄λ‹Ή ν•„λ“œλ₯Ό DB의 컬럼으둜 맀핑할 λ•Œ 이 정보λ₯Ό μ°Έκ³ ν•˜μ—¬ ν…Œμ΄λΈ”μ„ μƒμ„±ν•˜κ±°λ‚˜ μ—…λ°μ΄νŠΈ ν•œλ‹€.
이λ₯Ό 톡해 DB의 μŠ€ν‚€λ§ˆλ₯Ό κ΄€λ¦¬ν•˜λ©΄μ„œ ν•„λ“œμ˜ μ œμ•½μ‘°κ±΄μ„ λͺ…μ‹œμ μœΌλ‘œ 지정할 수 μžˆλ‹€.

@Builder

@BuilderλŠ” Lombok λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ μ œκ³΅ν•˜λŠ” μ• λ…Έν…Œμ΄μ…˜μ΄λ‹€.
이λ₯Ό μ‚¬μš©ν•˜λ©΄ λΆˆλ³€ 객체λ₯Ό μƒμ„±ν•˜λŠ” λΉŒλ” νŒ¨ν„΄μ„ μžλ™μœΌλ‘œ 생성할 수 μžˆλ‹€.

@Builder μ• λ…Έν…Œμ΄μ…˜μ„ 클래슀 λ ˆλ²¨μ— μ μš©ν•˜λ©΄ ν•΄λ‹Ή ν΄λž˜μŠ€μ— λŒ€ν•œ λΉŒλ” 클래슀λ₯Ό μƒμ„±ν•˜κ³ , 이 λΉŒλ” 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ 객체 생성 및 속성 값을 μ„€μ •ν•˜λŠ” λ©”μ„œλ“œ 체인 ν˜•μ‹μœΌλ‘œ ν‘œν˜„ν•  수 μžˆλ‹€.

@Builder의 μž₯점

  1. 가독성이 쒋아진닀.
    • 객체 생성 μ‹œμ— λ©”μ„œλ“œ 체인 ν˜•μ‹μœΌλ‘œ 속성 값을 μ„€μ •ν•  수 μžˆμ–΄ 가독성이 ν–₯μƒλœλ‹€.
  2. ν•„μˆ˜ 속성을 λͺ…μ‹œμ μœΌλ‘œ ν‘œν˜„ν•  수 μžˆλ‹€.
    • λΉŒλ” νŒ¨ν„΄μ—μ„œ ν•„μˆ˜λ‘œ μ„€μ •ν•΄μ•Ό ν•˜λŠ” 속성을 λͺ…μ‹œν•  수 μžˆμ–΄ 객체의 λΆˆλ³€μ„±μ„ 보μž₯ν•œλ‹€.
  3. 선택적 속성을 μœ μ—°ν•˜κ²Œ μ„€μ •ν•  수 μžˆλ‹€.
    • μ„ νƒμ μœΌλ‘œ μ„€μ •ν•  수 μžˆλŠ” 속성을 μ‰½κ²Œ μΆ”κ°€ν•  수 μžˆλ‹€.

μ˜ˆμ‹œ
[UserUpdateRequestDto]

    ...
    @Builder
    public UserUpdateRequestDto(Long id, String name, String phone, String address) {
        this.id = id;
        this.name = name;
        this.phone = phone;
        this.address = address;
    }
    ...


[UserControllerTest]

  ...
  @Test
      void update() {
        // given
        Long id = 3L;
        String name = "update_test2";
        String phone = "010-1111-1111";
        String address = "suwon";
  
        String url = "http://localhost:" + port + "/users/update/" + id;
  
        UserUpdateRequestDto requestDto = UserUpdateRequestDto.builder()
        .id(id)
        .name(name)
        .phone(phone)
        .address(address)
        .build();
  
        // when
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(requestDto), String.class);
  
    // then
    assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
  ...

참고자료

ꡐ재: μŠ€ν”„λ§λΆ€νŠΈμ™€ AWS둜 혼자 κ΅¬ν˜„ν•˜λŠ” μ›Ή μ„œλΉ„μŠ€

νƒœκ·Έ: ,

μΉ΄ν…Œκ³ λ¦¬:

μ—…λ°μ΄νŠΈ:

λŒ“κΈ€λ‚¨κΈ°κΈ°