프로젝트 생성(5장과 동일)
Swagger 설정(5.6절과 동일 - 변경사항 있음)
📌Spring Boot 버전 3.x.x 이상부터는 기존 SpringFox 호환 안됨
따라서 pom.xml파일에 아래와 같이 swagger 의존성 추가
// jpa/pom.xml
<dependencies>
...
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
...
</dependencies>
📌마찬가지로 SwaggerConfiguration도 다음과 같이 설정
config/SwaggerCofiguration.java
@Configuration
public class SwaggerConfiguration {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components())
.info(apiInfo());
}
private Info apiInfo() {
return new Info()
.title("Spring Boot JPA Documentation")
.description("스프링 부트 JPA 문서")
.version("1.0");
}
}
application.properties 설정
Spring Data JPA 의존성을 추가한 후 연동할 데이터 베이스의 정보를 작성해야 함
application.properties
// 데이터 베이스를 연동하는 설정
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3307/springboot
spring.datasource.username=root
spring.datasource.password=비밀번호
// 하이버네이트와 관련된 선택사항
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
ddl-auto: 데이터베이스를 자동으로 저작하는 옵션
* 사용가능한 다른 옵션
=> 운영환경: validate나 none 사용
=> 개발환경: create나 update 사용
Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요 없음
이 기능을 가능하게 하는 것이 바로 엔티티임
엔티티
JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스로, 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의함. 엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있음
아래 테이블을 간단하게 엔티티 클래스로 구현가능함
상품 테이블 | |
상품 번호 | int |
상품 이름 | varchar |
상품 가격 | int |
상품 재고 | int |
상품 생성 일자 | DateTime |
상품 정보 변경 일자 | DateTime |
data/entity/Product.java
import lombok.Getter;
import lombok.Setter;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name="product")
@Getter
@Setter
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 createAt;
private LocalDateTime updateAt;
... getter/setter 메서드 생략 ...
}
application.properties에 정의한 spring.jpa.hibernate.ddl-auto의 값을 create로 설정하면 쿼리문을 작성하지 않아도 데이터베이스에 테이블이 자동으로 만들어짐
엔티티 관련 기본 어노테이션
@Entity
해당 클래스가 엔티티임을 명시하기 위한 어노테이션
클래스 자체는 테이블과 일대일로 매칭되며, 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미함
@Table
테이블과 클래스의 이름을 다르게 지정할 때 사용
보통 엔티티 클래스는 테이블과 매핑되므로, 특별한 경우가 아니면 이 어노테이션은 필요 없음
서로 다른 이름을 쓰려면 @Table(name = 값) 형태로 데이터베이스의 테이블명을 따로 명시해야 함
@Id
엔티티 클래스의 필드는 테이블의 칼럼과 매핑. 이 어노테이션이 선언된 필드는 테이블의 기본값 역할로 사용됨
모든 엔티티는 @Id 어노테이션이 필요함
@GeneratedValue
해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용함(일반적으로 @Id 어노테이션과 함께 사용)
📌 값 생성방식
@Column
필드에 몇 가지 설정을 더할 때 사용함
(엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑됨. 따라서 별다른 설정을 안 한다면 이 어노테이션 필요 없음)
@Transient
엔티티 클래스에는 선언되어 있는 필드지만 DB에서는 필요 없을 경우, 이 어노테이션을 사용하여 DB에서 이용하지 않게 할 수 있음
Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 DB를 사용할 수 있는 아키텍처를 제공함
(스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면, 기존의 다양한 메서드를 손쉽게 활용 가능)
리포지토리 인터페이스 생성
📌리포지토리 : Spring Data JPA가 제공하는 인터페이스로 엔티티가 생성한 DB에 접근하는 데 사용됨
리포지토리를 생성하려면 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스 생성 후 JpaRepository 상속받으면 됨
(이때, 대상 엔티티(Product)와 기본값 타입(Long)을 지정해야 함)
이렇게 하면 별도의 메서드 구현 없이도 많은 기능이 제공됨
data/repository/ProductRepository.java
public interface ProductRepository extends JpaRepository<Product, Long> { }
리포지토리 메서드의 생성 규칙
📌DAO(Data Access Object)
DB에 접근하기 위한 로직을 관리하는 객체로, 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능을 수행함
(다만, 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있음)
DAO 클래스 생성
DAO 클래스는 일반적으로 '인터페이스 - 구현체' 구성으로 생성함.
의존성 결합을 낮추기 위한 디자인 패턴으로, 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성함.
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;
}
일반적으로 DB에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달함. 이때 데이터 객체를 엔터티 객체로 할지, DTO 객체로 할지는 개발자마다 입장 다름
일반적인 설계 원칙: 엔티티 객체는 DB에 접근하는 계층에서만 사용하도록 정의하고, 다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용함
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 org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
@Component // 스프링 빈 등록
public class ProductDAOImpl implements ProductDAO {
// DAO 객체가 DB에 접근하기 위해 리포지토리 인터페이스를 사용해 의존성을 주입 받음
private final ProductRepository productRepository;
@Autowired
public ProductDAOImpl(ProductRepository productRepository){
this.productRepository = productRepository;
}
// 인터페이스를 정의한 메서드 구현
@Override
public Product insertProduct(Product product) { // Product 엔티티를 DB에 저장
Product savedProduct = productRepository.save(product);
return savedProduct;
}
@Override
public Product selectProduct(Long number) { // 조회 메서드
Product savedProduct = productRepository.getById(number);
return savedProduct;
}
@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.setUpdateAt(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();
}
}
}
앞서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 함.
이를 위해 먼저 DAO의 메서드를 호출하고 그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후, 컨트롤러를 생성하도록 함.
서비스 레이어에서는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공함
① DTO 클래스 작성
package com.springboot.jpa.data.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
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;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
}
package com.springboot.jpa.data.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
}
② 서비스 인터페이스 작성
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;
}
위 인터페이스는 DAO에서 구현한 기능을 서비스 인터페이스에서 호출해 결괏값을 가져오는 작업을 수행하도록 설계함
(서비스에서는 클라이언트가 요청한 데이터를 가공해서 컨트롤러에게 넘김)
- 리턴 타입이 DTO 객체임 => 서비스 레이어에서 DTO 객체와 앤티티 객체를 각 레이어에 변환해서 전달하는 역할도 수행.
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 org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
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.setUpdateAt(LocalDateTime.now());
product.setCreateAt(LocalDateTime.now());
Product savedProduct = productDAO.insertProduct(product);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setStock(savedProduct.getStock());
productResponseDto.setName(savedProduct.getName());
productResponseDto.setNumber(savedProduct.getNumber());
productResponseDto.setPrice(savedProduct.getPrice());
return productResponseDto;
}
@Override
public ProductResponseDto changeProductName(Long number, String name) throws Exception{ // 업데이트 메서드(상품정보 중 이름 변경)
Product changeProduct = productDAO.updateProductName(number, name);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(changeProduct.getNumber());
productResponseDto.setName(changeProduct.getName());
productResponseDto.setStock(changeProduct.getStock());
productResponseDto.setPrice(changeProduct.getPrice());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) throws Exception{ // 상품 삭제 메서드
productDAO.deleteProduct(number);
}
}
컨트롤러 생성
서비스 객체의 설계를 마친 후에는 비즈니스 로직과 클라이언트 요청을 연결하는 컨트롤러를 생성함
(+ 컨트롤러에서 사용할 ChangeProductNameDtro도 생성)
// controller/ProductController.java
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("정상적으로 삭제되었습니다.");
}
}
package com.springboot.jpa.data.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChangeProductNameDto {
private Long number;
private String name;
}
Swagger API를 통한 동작 확인
📌애플리케이션 실행 후 http://localhost:8080/swagger-ui/index.html 로 접속
상품 정보 저장
createProduct() 메서드 사용
상품 정보 조회
createProduct() 메서드 사용
상품 정보 변경
updateProductName() 메서드 사용
Swagger에서 Boby에 식별자 번호와 바꾸고자 하는 이름 기입
상품 정보 삭제
deleteProduct() 사용
삭제하려는 number값을 파라미터에 입력
롬복(Lombok)
데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리
📌 장점
1. JPA에서 ( 엔티티 )는 DB의 테이블에 대응하는 클래스로, DB에 쓰일 테이블과 칼럼을 정의한다.
2. 모든 엔티티에 꼭 필요한 어노테이션은 ( @Id )이다.
3. Spring Data JPA는 ( JpaRepository )를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공한다.
4. DB에 접근하기 위한 로직을 관리하는 객체는 ( DAO )이다.
5. DAO 클래스는 일반적으로 ( 인터페이스 ) - ( 구현체 ) 구성으로 생성한다.
6. JPA는 값을 갱신할 때 update 키워드를 사용하지 않고 ( 영속성 컨텍스트 ) 를 활용한다.
7. ( 롬복 )은 데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리이다.
1. 다음은 Product 엔티티 클래스이다. 빈칸에 들어갈 알맞은 코드를 작성하시오.
import lombok.Getter;
import lombok.Setter;
import jakarta.persistence.*;
import java.time.LocalDateTime;
// 빈칸 1)
@Table(name="product")
@Getter
@Setter
public class Product {
// 빈칸 2)
// 빈칸 3)
private Long number;
// 빈칸 4)
private String name;
// 빈칸 4)
private Integer price;
// 빈칸 4)
private Integer stock;
private LocalDateTime createAt;
private LocalDateTime updateAt;
... getter/setter 메서드 생략 ...
}
2. ProductDAO 인터페이스의 구현체 클래스의 일부인 상품생성 메서드 insertProduct()를 구현해라
@Override
public Product insertProduct(Product product) { // Product 엔티티를 DB에 저장
Product savedProduct = // productRepository 사용
return savedProduct;
}
1.
import lombok.Getter;
import lombok.Setter;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity // 1
@Table(name="product")
@Getter
@Setter
public class Product {
@Id // 2
@GeneratedValue(strategy = GenerationType.IDENTITY) // 3
private Long number;
@Column(nullable = false) // 4
private String name;
@Column(nullable = false) // 4
private Integer price;
@Column(nullable = false) // 4
private Integer stock;
private LocalDateTime createAt;
private LocalDateTime updateAt;
... getter/setter 메서드 생략 ...
}
2.
@Override
public Product insertProduct(Product product) { // Product 엔티티를 DB에 저장
Product savedProduct = productRepository.save(product);
return savedProduct;
}
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.104-158.
Corner Spring 1
Editor : via
[스프링 1팀] 7장. 테스트 코드 작성하기 (0) | 2024.12.27 |
---|---|
[스프링 1팀] 5장~6.5장. API 작성과 데이터베이스 연동 (0) | 2024.11.22 |
[스프링 1팀] 1장~4장. 스프링 부트 개발 환경과 애플리케이션 개발하기 (0) | 2024.11.15 |
[스프링 1팀] 스프링 입문 섹션 7~8 (0) | 2024.11.08 |
[스프링 1팀] 스프링 입문 섹션 5~6 (0) | 2024.10.11 |