애플리케이션은 데이터를 주고받는 것이 주 목적이다. 마리아DB를 애플리케이션에 적용해 보자.
6.1 마리아DB 설치
나는 M1 MacBook Pro를 사용하기 때문에 homebrew로 마리아db, 서드파티 도구로 HeidiSQL이 아닌 Sequel Pro를 설치해 주었다.
6.2 ORM
Object Relational Mapping의 줄임말로 객체 관계 매핑을 의미한다.
= 클래스와 RDB(Relational Database)의 테이블을 자동으로 매핑하는 방법이다.
스프링부트 애플리케이션 <--> ORM <--> 데이터베이스
ORM을 이용하면 쿼리문 작성이 아닌 코드로 데이터를 조작할 수 있다.
6.3 JPA
Java Persistence API는 자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다.
= ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함한다.
JPA 또한 실제로 동작하는 것이 아니고 어떻게 동작해야 하는지 메커니즘을 정리한 표준 명세이다.
스프링부트 애플리케이션 <--> JPA <--> 데이터베이스
= JPA의 역할이 ORM이라고 보면 됨
6.4 하이버네이트
6.5 영속성 컨텍스트
6.6 데이터베이스 연동
Spring Data JPA 의존성을 추가한 후에는 별도의 설정이 필요하다.
즉, 애플리케이션이 정상적으로 실행될 수 있게 연동할 데이터베이스의 정보를 application.properties에 작성해야 한다.
6.7 엔티티 설계
JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의한다.
엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.springboot.jpa.data.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
... getter/setter ...
}
|
cs |
6.7.1 엔티티 관련 기본 어노테이션
@Entity : 클래스가 엔티티임을 명시한다. 테이블과 일대일 매칭이며 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드이다.
@Table : 클래스와 테이블의 이름을 다르게 지정해야 하는 경우에 사용한다.
@Id : 테이블의 기본값 역할로 사용된다. 모든 엔티티는 @Id 어노테이션이 필요하다.
@GenerateValue : 해당 필드 값을 어떤 방식으로 생성할지 결정할 때 사용한다.
@Column : 필드에 몇 가지 설정을 더할 때 사용한다. ex) name, nullable, length, unique
@Transient : ??
6.8 리포지토리 인터페이스 설계
Spring Data JPA는 JpaRepository를 기반으로 데이터베이스를 사용할 수 있는 아키텍쳐를 제공한다.
스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.
1
2
3
4
5
6
7
8
|
package com.springboot.jpa.data.repository;
import com.springboot.jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
|
cs |
6.8.1 리포지토리 인터페이스 설계
Repository는 Spring Data JPA가 제공하는 인터페이스이다. 엔티티가 생성한 데이터베이스에 접근하는 데 사용된다.
JapRepository를 상속받을 때는 대상 엔티티와 기본값 타입을 지정해야 한다. 생성된 리포지토리는 다양한 메서드를 제공한다.
6.8.2 리포지토리 메서드의 생성 규칙
리포지토리에서는 몇 가지 명명규칙에 따라 커스텀 메서드도 생성할 수 있다.
- FindBy
- And, Or
- Like / NotLike
- StartsWith / StartingWith
- EndsWith / EndingWith
- IsNull / IsNotNull
- True / False
- Before / After
- LessThen / GreaterThen
- Between
- OrderBy
- countBy
6.9 DAO 설계
Data Access Object는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다.
DAO와 리포지토리는 역할이 비슷하다.
6.9.1 DAO 클래스 생성
DAO 클래스는 일반적으로 '인터페이스 - 구현체' 구성으로 생성한다. 의존성 결합을 낮추기 위한 디자인 패턴이다.
데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체럴 전달한다.
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.springboot.jpa.data.dao;
import com.springboot.jpa.data.entity.Product;
public interface ProductDAO {
Product insertProduct(Product product);
Product selectProduct(Long number);
Product updateProductName(Long number, String name) throws Exception;
void deleteProduct(Long number) throws Exception;
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package com.springboot.jpa.data.dao.impl;
import com.springboot.jpa.data.dao.ProductDAO;
import com.springboot.jpa.data.entity.Product;
import com.springboot.jpa.data.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.Optional;
public class ProductDAOImpl implements ProductDAO {
private final ProductRepository productRepository;
@Autowired
public ProductDAOImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Product insertProduct(Product product) {
Product savedProduct = productRepository.save(product);
return savedProduct;
}
@Override
public Product selectProduct(Long number) {
Product selectedProduct = productRepository.getById(number);
return selectedProduct;
}
@Override
public Product updateProductName(Long number, String name) throws Exception {
Optional<Product> selectedProduct = productRepository.findById(number);
Product updatedProduct;
if (selectedProduct.isPresent()) {
Product product = selectedProduct.get();
product.setName(name);
product.setUpdatedAt(LocalDateTime.now());
updatedProduct = productRepository.save(product);
} else {
throw new Exception();
}
return updatedProduct;
}
@Override
public void deleteProduct(Long number) throws Exception {
Optional<Product> selectedProduct = productRepository.findById(number);
if (selectedProduct.isPresent()) {
Product product = selectedProduct.get();
productRepository.delete(product);
} else {
throw new Exception();
}
}
}
|
cs |
6.10 DAO 연동을 위한 컨트롤러와 서비스 설계
앞에서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 한다.
DAO의 메서드를 호출하고 그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후 컨트롤러를 생성한다.
6.10.1 서비스 클래스 만들기
서비스 레이어에서는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공한다.
핵심 기능을 구현하기 위해 세부 기능을 정의해야 하는데, 서비스 객체는 DAO와 마찬가지로 추상화해서 구성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.springboot.jpa.data.dto;
public class ProductDto {
private String name;
private int price;
private int stock;
public ProductDto(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
.. getter / setter ...
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.springboot.jpa.data.dto;
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
public ProductResponseDto() {}
public ProductResponseDto(Long number, String name, int price, int stock) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
}
.. getter / setter ...
}
|
cs |
기본적인 CRUD 기능을 호출하기 위해 메서드도 정의한다.
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.springboot.jpa.service;
import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;
public interface ProductService {
ProductResponseDto getProduct(Long number);
ProductResponseDto saveProduct(ProductDto productDto);
ProductResponseDto changeProductName(Long number, String name) throws Exception;
void deleteProduct(Long number) throws Exception;
}
|
cs |
위 인터페이스는 DAO에서 구현한 기능을 서비스 인터페이스에서 호출해 결괏값을 가져오는 작업을 수행하도록 했다.
서비스 패키지에서는 클라이언트가 요청한 데이터를 적절하게 가공해 컨트롤러에게 넘기는 역할을 한다.
데이터베이스와 긴밀한 데이터 엑세스 레이어는 엔티티 객체를 사용하고, 클라이언트와 가까워지는 레이어는 DTO 객체를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package com.springboot.jpa.service.impl;
import com.springboot.jpa.data.dao.ProductDAO;
import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;
import com.springboot.jpa.data.entity.Product;
import com.springboot.jpa.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
public class ProductServiceImpl implements ProductService {
private final ProductDAO productDAO;
@Autowired
public ProductServiceImpl(ProductDAO productDAO) {
this.productDAO = productDAO;
}
@Override
public ProductResponseDto getProduct(Long number) {
Product product = productDAO.selectProduct(number);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(product.getNumber());
productResponseDto.setName(product.getName());
productResponseDto.setPrice(product.getPrice());
productResponseDto.setStock(product.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto saveProduct(ProductDto productDto) {
Product product = new Product();
product.setName(productDto.getName());
product.setPrice(productDto.getPrice());
product.setStock(productDto.getStock());
product.setCreatedAt(LocalDateTime.now());
product.setUpdatedAt(LocalDateTime.now());
Product savedProduct = productDAO.insertProduct(product);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(savedProduct.getNumber());
productResponseDto.setName(savedProduct.getName());
productResponseDto.setPrice(savedProduct.getPrice());
productResponseDto.setStock(savedProduct.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto changeProductName(Long number, String name) throws Exception {
Product changedProduct = productDAO.updateProductName(number, name);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(changedProduct.getNumber());
productResponseDto.setName(changedProduct.getName());
productResponseDto.setPrice(changedProduct.getPrice());
productResponseDto.setStock(changedProduct.getStock());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) throws Exception {
productDAO.deleteProduct(number);
}
}
|
cs |
DAO 인터페이스를 선언하고 @Autowired를 지정한 생성자를 통해 의존성을 주입받는다.
서비스 레이어에는 DTO 객체와 엔티티 객체가 공존하도록 설계되어 있기 때문에 DTO 객체를 생성하고 값을 넣어 초기화해 준다.
6.10.2 컨트롤러 생성
서비스 객체 설계를 마친 후에는 비즈니스 로직과 클라이언트의 요청을 연결하는 컨트롤러를 생성해야 한다,
컨트롤러는 요청과 응답을 전달하는 역할만 맡는 것이 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package com.springboot.jpa.controller;
import com.springboot.jpa.data.dto.ChangeProductNameDto;
import com.springboot.jpa.data.dto.ProductDto;
import com.springboot.jpa.data.dto.ProductResponseDto;
import com.springboot.jpa.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping()
public ResponseEntity<ProductResponseDto> getProduct(Long number) {
ProductResponseDto productResponseDto = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
@PostMapping()
public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto productDto) {
ProductResponseDto productResponseDto = productService.saveProduct(productDto);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
@PutMapping()
public ResponseEntity<ProductResponseDto> changeProductName(
@RequestBody ChangeProductNameDto changeProductNameDto) throws Exception {
ProductResponseDto productResponseDto = productService.changeProductName(
changeProductNameDto.getNumber(),
changeProductNameDto.getName());
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
@DeleteMapping()
public ResponseEntity<String> deleteProduct(Long number) throws Exception {
productService.deleteProduct(number);
return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
}
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.springboot.jpa.data.dto;
public class ChangeProductNameDto {
private Long number;
private String name;
public ChangeProductNameDto(Long number, String name) {
this.number = number;
this.name = name;
}
public ChangeProductNameDto() {}
... getter / setter ...
}
|
cs |
6.10.3 Swagger API를 통한 동작 확인
왜 나는 안 되지... ㅠ
6.11 반복되는 코드의 작성을 생략하는 방법 - 롬복
반복적으로 사용하는 getter / setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리이다.
@Getter, @Setter : 클래스에 선언되어 있는 필드에 대한 getter / setter 메서드를 생성한다.
@NoArgsConstructer : 매개변수가 없는 생성자를 자동 생성한다.
@AllArgsConstructer : 모든 필드를 매개변수로 갖는 생성자를 자동 생성한다.
@RequiredArgsConstructor : 필드 중 final이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동 생성한다.
@ToString : toString() 메서드를 생성한다.
@EqualsAndHashCode : 객체의 동등성*과 동일성*을 비교하는 연산 메서드를 생성한다.
@Data : @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode를 모두 포괄한다.
*동등성 : 두 객체가 가진 값이 같음
*동일성 : 두 객체가 같은 객체임
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package com.springboot.jpa.data.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ProductDto {
private String name;
private int price;
private int stock;
}
|
cs |
from 스프링 부트 핵심 가이드: 스프링 부트를 활용한 애플리케이션 개발 실무 (장정우, 위키북스)
'Spring Boot' 카테고리의 다른 글
2022 GDSC Spring Study 입문 - 1주차 (1) | 2022.10.02 |
---|---|
05. API를 작성하는 다양한 방법 (0) | 2022.08.09 |
04. 스프링 부트 애플리케이션 개발하기 (0) | 2022.08.08 |
02. 개발에 앞서 알면 좋은 기초 지식 (0) | 2022.08.03 |
01. 스프링 부트란? (0) | 2022.08.03 |