Group Study (2022-2023)/Spring 입문

[Spring 입문] 2주차 스터디 - 스프링 부트에서 JPA로 데이터베이스 다뤄보기

비냐 2022. 10. 10. 00:12

목차

  1. JPA
  2. 프로젝트에 Spring Data JPA 적용하기
  3. Spring Data JPA 테스트 코드 작성하기
  4. 등록/수정/조회 API 만를기
  5. JPA Auditing으로 생성시간/수정시간 자동화하기
  6. 더 알아보기

 


3. 스프링 부트에서 JPA로 데이터베이스 다뤄보자

3.1 JPA 소개

JPA 등장 배경

현대의 웹 애플리케이션에서 관계형 데이터베이스(RDB)는 빠질 수 없는 요소

→ 객체를 관계형 데이터베이스에서 관리하는 것이 중요!

→ 관계형 데이터베이스가 SQL만 인식할 수 있기 때문에 모든 코드가 SQL 중심으로

→ 반복적인 SQL의 생성, 수십, 수백 개의 테이블의 명령어를 반복 & 객체지향 프로그래밍 언어와의 패러다임이 달라 데이터를 저장할 수 없음

→ 상속, 1:N 관계 등 다양한 객체 모델링을 데이터베이스로 구현할 수 없음

⇒ 서로가 지향하는 바가 다른 2개 영역의 중간에서 패러다임을 일치시켜 주기 위한 기술

⇒ 개발자는 객체 지향적인 프로그래밍, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성

 

Spring Data JPA

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것에 큰 차이가 없음에도 이를 권장하는 이유

  1. 구현체 교체의 용이성

Hibernate 외의 다른 구현체로 쉽게 교체하기 위함 Spring Data JPA 내부에서 구현체 매핑을 지원해주기 때문에 새로운 JPA 구현체가 대세로 떠오를 경우 쉽게 교체가 가능하다.

    2. 저장소 교체의 용이성

관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함 점점 트래픽이 많아져 관계형 데이터베이스로 도저히 감당히 안 될 경우 다른 DB로 의존성만 교체하면 된다. → Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문

 

실무에서 JPA

장점

  • CRUD 쿼리를 직접 작성할 필요가 없음
  • 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등의 객체지향 프로그래밍을 쉽게 할 수 있음

단점

  • 높은 러닝 커브로 인해 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해해야 함

3.2 프로젝트에 Spring Data JPA 적용하기

dependencies {
	//h2
	implementation 'com.h2database:h2:1.4.197'

	//Spring Data Jpa
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

1. spring-boot-starter-data-jpa : 스프링부트용 Spring Data 추상화 라이브러리

2. h2 : 인메모리 관계형 데이터베이스, 별도의 설치 없이 의존성만으로 관리할 수 있고 메모리에서 실행되기 때문에 재시작할 때마가 초기화 되어 테스트 용도로 많이 사용

도메인? 소프트웨어에 대한 요구사항 혹은 문제 영역

//Posts.java
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

1. @Entity : 테이블과 링크된 클래스임을 나타내는 어노테이션. 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매칭함

2. @Id : 해당 테이블의 PK 필드

3. @GeneratedValue : PK의 생성 규칙. 스프링 부트 2.0 에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment 가 된다.

4. @Column : 테이블의 칼럼. 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 됨.

5. @NoArgsConstructor : 기본 생성자 자동 추가

6. @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동생성

7. @Builder : 해당 클래스의 빌더 패턴 클래스를 생성

⇒ Lombok 어노테이션, 코드 변경량을 최소화시켜 줌

주요 어노테이션을 클래스에 가깝게 두는 이유
이후에 새 언어 전환으로 롬복 등의 의존성이 더이상 필요 없을 경우 쉽게 삭제가 가능

Entity의 PK를 Auto_increment를 추천하는 이유

  1. FK를 맺을 때 다른 테이블에서 복합키 전부를 가지고 있거나, 중간 테이블을 하나 더 둬야하는 상황이 발생할 수 있어서
  2. 인덱스에 좋은 영향을 끼치지 못해서
  3. 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생해서

Setter 메소드가 없는 이유
setter 를 무작정 생성할 경우 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경 시 복잡해짐
→ 해당 필드의 값 변경이 필요하면 명확히 목적과 의도를 나타낼 수 있는 메소드를 추가해야함

그렇다면 어떻게 값을 채워 DB에 삽입하나요?

  1. 생성자를 통해 최종값을 채운 후 DB에 삽입, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경함
  2. @Builder를 통해 제공되는 빌더 클래스를 사용하여 어느 필드에 어떤 값을 채워야할지 명확하게 인지할 수 있도록 함

 

//PostsRepository.java
public interface PostsRepository extends JpaRepository<Posts, Long> {}

Dao라고 불리는 DB Layer 접근자

단순히 인터페이스를 생성 후, JpaRepository<Entity 타입, PK 타입> 를 상속하면 기본적인 CRUD 메소드가 자동 생성

 ❗ 주의할 점
Entity 클래스와 Entity Repository는 같은 패키지에 함께 위치해야함
→ 둘은 아주 밀접한 관계이고 Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수 없기 때문


3.3 Spring Data JPA 테스트 코드 작성하기

//PostsRepositoryTest.java
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @AfterEach
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void loadSavedWriting() {
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder()
                .title(title)
                .content(content)
                .author("yjin1100@sookmyung.ac.kr")
                .build());

        List<Posts> postsList = postsRepository.findAll();

        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }
}

1. @AfterEach : JUnit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정, 테스트 간 데이터 침범을 막기 위해 사용

2. postsRepository.save : 테이블 posts에 insert/update 쿼리를 실행. id값이 있다면 update, 없다면 insert 쿼리 실행

3. postsRepository.findAll : 테이블 posts에 있는 모든 데이터를 조회해오는 메소드

별다른 설정 없이 @SpringBootTest를 사용할 경우 H2 데이터베이스를 자동으로 실행함

 

실제로 실행된 쿼리 콘솔에서 형태 보는 법

application.properties 파일에 spring.jpa.show_sql=true옵션 추가

오류가 나는 것 같아요
h2를 최신 버전을 사용하면 적용이 안되는 것 같다. gradle에서 버전을 1.4.197로 변경할 것

 

출력되는 쿼리 로그를 MySQL 버전으로 바꾸는 법

application.properties 파일에 Spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect 옵션 추가

 


3.4 등록/수정/조회 API 만들기

API 를 만들기위해서는

  1. Request 데이터를 받을 Dto
  2. API 요청을 받을 Controller
  3. 트랜잭션, 도메인 기능간의 순서를 보장하는 Service

이렇게 3가지의 클레스가 필요함.

 

이 클래스들은 각각 아래의 스프링 웹 계층에서 동작함.

각각의 레이어가 하는 일에 대해 설명하자면,

Web Layer

  • 컨트롤러(@ Contoroller), JSP/Freemaker등의 뷰 템플릿 영역
  • 외부 요청과 응답에 대한 전반적인 영역을 이야기함

 

 

Service Layer

  • @ Service에 사용되는 서비스 영역
  • Controller와 Dao 의 중간 영역에서 사용
  • @ Transaction이 사용되는 영역

 

 

Repository Layer

  • 데이터베이스와 같은 데이터 저장소에 접근하는 영역

 

 

DTOs

  • DTO = Data Transfer Object = 계층간 데이터 교환을 위한 객체 ex. 뷰 템플릿 엔진에서 사용할 객체나 Repository Layer에서 결과로 넘겨준 객체 등등
  • Dtos : Dto의 영역

 

 

Domain Model

  • 도메인이라고 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할수 있고 공유할 수있도록 단순화 시킨것 ex. 택시 앱 → 배차, 탑승, 요금 등등이 도메인이 될 수있음.
  • @Entity가 사용된 영역 역시 Domain Model 이나 무조건 데이터베이스 테이블과 관계가 있어야하는 것은 아님.
  • 5가지 레이어 중 비즈니스 처리를 담당하는 곳

 

왜 Domain Model에서 비즈니스 로직을 처리하는 걸까?

이유는 기존의 Service Layer에 몽땅 로직을 우겨넣으면, 계층 구조가 무의미해지기 때문.

간단하게 주문취소 로직을 생각해 보자.

주문을 취소하려면 먼저

  1. 데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송 정보(Delivery)를 조회하고
  2. 배송취소를 해야하는지 확인
  3. 배송중이라면 → 배송 취로로 변경
  4. 각 테이블에 취소 상태를 업데이트

하는 과정을 거친다. 이 로직을

서비스 클래스에서 처리하는 코드

@Transactional
public Order cancelOrder(int orderId){

	//1)데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송 정보(Delivery)를 조회하고
	OrdersDto order = ordersDao.selectOrders(orderId);
	BillingDto billing = billingDao.selectBilling(orderId);
	DeliveryDto delivery = deliveryDao.selectDelivery(orderId):

	//2)배송취소를 해야하는지 확인
	String deliveryStatus = delivert.getStatus();

	//3)배송중이라면 → 배송 취로로 변경
	if("IN_PROGRESS".equals(deliveryStatus)){
		delivery.setStatus("CANCEL");
		delivery.update(delivery);
	}

	//4)각 테이블에 취소 상태를 업데이트
	order.setStatus("CANCEL");
	ordersDao.update(order);

	billing.setStatus("CANCEL");
	deliveryDao.update(billing);

	return order;
}

도메인 모델에서 처리하는 코드

@Transactional
public Order cancelOrder(int orderId){

	//1)데이터베이스로부터 주문정보(Orders), 결제정보(Billing), 배송 정보(Delivery)를 조회하고
	Orders order = ordersRepository.findById(orderId);
	Billing billing = billingRepository.findById(orderId);
	Delivery delivery = deliveryRepository.findById(orderId);

	//2)배송취소를 해야하는지 확인 3)배송중이라면 → 배송 취로로 변경
	delivery.cancel();

	//4)각 테이블에 취소 상태를 업데이트
	order.cancel();
	billing.cancel();

	return order;
}

도메인 모델에서 비즈니스 로직을 처리하면, order, billing, delivery가 각자 본인의 취소이벤트 처리를 하게 되고 서비스메소드는 순서만 보장한다.

+) 추가 : 절대 Entity 클래스를 Request/Response 클래스로 사용하면안됨

: 이유 : 엔티티는 데이터베이스와 맞닿은 핵심 클래스이기때문.

Entity 기준으로 테이블이 생성되고 스키마가 변경됨 = 수많은 서비스 클래스나 비즈니스 로직이 엔티티 클래스를 기준으로 동작
→ 한번 엔티티 클래스가 변경되면 이어서 수정할 것이 너무 많음.
→ 따라서 자주 수정해야하는 Request, Response 등은 따로 Request, Response Dto를 만들기.

즉 View Layer와 DB Layer의 역할분리를 철처하게 해야한다.

 


 

3.5 JPA  Auditing으로 생성시간/수정시간 자동화하기

보통 Entity에는 헤당 데이터의 생성시간과 수정시간을 포함. 그러다보니 DB에 insert 하기전에 날짜 데이터를 수정또는 등록하는 코드가 여기저기 들어감. 이를 하나로 깔끔하게 정리하기위해 JPA Auditing을 쓸 것.

쓸 날짜 타입은 Java8 부터 등장한 LocalDate, LocalDateTime 이다.

날짜타입을 생성 및 수정 메소드를 가진 추상 클래스를 만들고 Entity가 이를 상속하도록 함으로 날짜 등록 및 수정 기능을 추가한다.

  • 날짜타입을 생성 및 수정 메소드를 가진 추상 클래스(src/main/java/com/jojoldu/book/springboot/domain/BasTimeEntity)
package com.jojoldu.book.springboot.domain.posts;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

1. @MappedSupercalss :  JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우 필드들도 칼럼으로 인식하도록 함.

2. @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에 Auditing 기능을 포함시킴

3. @CreatedDate : Entity가 생성되어 저장될때 시간이 자동 저장

4. @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장

 

위의 BaseTimeEntity를 Posts 클래스가 상속받도록 변경한뒤, JPA Auditing 어노테이션들을 활성화하도로고 Application 클래스에 활성화 어노테이션

 @EnableJpaAuditing  : JPA Auditing 활성화

을 추가하면 코드 작성은 끝.

 

- 더 알아보기

1. 어노테이션이란?

어노테이션을 이해하기 위해서는 먼저 메타데이터가 무엇인지 알 필요가 있다. 메타데이터는 데이터에 대한 속성 정보로, 하위 레벨 데이터를 설명 및 기술하는 데이터라고 이해할 수 있다. 그리고 어노테이션은 프로그램에게 추가적인 정보를 제동해주는 메타데이터라고 볼 수 있다.

어노테이션의 용도는 다음과 같다.

  • 코드 문법 에러 체크
  • 코드 자동 생성 정보 제공
  • 런타임시 특정 기능을 실행하는 정보 제공

어노테이션은 클래스와 메서드에 추가되어 클래스 역할 정의, Bean 주입, getter 및 setter 자동 생성 등의 다양한 기능을 부여할 수 있다.

2. 스프링에서 주로 사용하는 어노테이션

@Componenet

개발자가 정의한 클래스를 Bean으로 등록할 때 사용하는 어노테이션.

@Component
public class Fruit{
	public Fruit(){
    	system.out.println("fruit")
    }
}

@Component(value="apple")
public class Fruit{
	public Fruit(){
    	system.out.println("fruit")
    }
}

위 코드는 클래스 이름을 카멜케이스로 변경한 것이 Bean id로써 사용되고, 아래 코드는 value를 통해 Bean의 이름이 지정된다.

@ComponentScan

@Component, @Service, @Repository, @Controller, @Configuration이 붙은 클래스 Bean들을 찾아서 Context에 Bean으로 등록하는 어노테이션.

@Bean

개발자가 제어 불가능한 외부 라이브러리와 같은 것들을 Bean으로 만들 때 사용하는 어노테이션. 라이브러리를 Bean으로 등록할 때는 해당 라이브러리를 반환하는 메서드를 만든 후 @Bean 어노테이션을 사용한다. 아무런 값을 지정하지 않으면 메서드 이름을 카멜케이스로 변경한 것이 Bean id로 등록된다.

@Service

클래스가 서비스 역할을 한다고 명시하기 위해 사용하는 어노테이션. 비즈니스 로직을 수행하는 클래스라는 것을 나타내는 용도로 쓰인다.

@Controller

해당 클래스가 Controller의 역할을 한다고 명시하기 위해 사용하는 어노테이션. Spring MVC에서 Controller 클래스에 사용된다.

@RequestMapping

요청 URL을 어떤 메서드가 처리할지 mapping 해주는 어노테이션. Controller나 Controller의 메서드에 적용하고, 요청을 받는 형식인 GET/POST/PATCH/PUT/DELETE를 정의한다. 요청받는 형식을 정의하지 않는다면 자동적으로 GET으로 설정된다. 클래스 단위에 적용하면 하위 메소드에 모두 적용된다.

@Autowired

생성자, setter, 필드의 3가지 경우에서 사용하는 어노테이션. 타입에 따라 알아서 Bean을 주입해준다. 타입이 없으면 이름을 확인한다. 

@Configuration

해당 클래스가 Bean 구성 클래스임을 알려주는 어노테이션. @Configuration을 클래스에 적용하고 @Bean을 해당 클래스의 메서드에 적용하면 @Autowired로 Bean을 부를 수 있다.

@Qualifier("id123")

같은 타입의 Bean 객체가 있을 때 아이디를 적어 원하는 Bean이 주입될 수 있도록 하는 어노테이션. @Autowired와 같이 쓰인다.

@Test

JUnit에서 테스트 할 대상을 표시하는 어노테이션.

3. 롬복에서 주로 사용하는 어노테이션

롬복은 반복되는 getter, setter 등의 반복 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리로, 주로 사용하는 어노테이션은 다음과 같다.

@Setter

클래스 내 모든 필드의 Setter 메서드를 자동으로 생성하는 어노테이션.

@Getter

클래스 내 모든 필드의 Getter 메서드를 자동으로 생성하는 어노테이션.

@ToString

클래스 내 모든 필드의 toString 메서드를 자동으로 생성하는 어노테이션. 

@ToString(exclude = "password")

위와 같이 사용할 경우, 특정 필드를 toString() 결과에서 제외한다.

@NoArgsConstructor

기본 생성자를 자동으로 추가하는 어노테이션. JPA에서 Entity 클래스를 생성하는 것을 허용하기 위해 사용한다. 주로 Dto 클래스 상단에 추가한다.

@AllArgsConstructor

모든 필드 값을 파라미터로 받는 생성자를 추가한다.