5장에서 생성한 방식과 동일하게 프로젝트 생성
- 버전: 2.5.6, groupId: 'com.springboot', name: 'jpa', artifactId: 'jpa',
- 의존성 선택 단계: Developer Tools: Lombok, Spring Configuration Processor, Web: Spring Web, SQL: Spring Data JPA, MariaDB Driver
5.6절에서 진행했던 Swagger 의존성을 pom.xml 파일에 추가
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
소스코드: config/SwaggerConfiguration.java
리소스: logback-spring.xml
SwaggerConfiguration 파일을 그대로 복사하면 api() 메서드 내의 basePackage가 일치하지 않아 정상적으로 동작하지 않음, 아래와 같이 수정
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.springboot.jpa"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot Open API Test with Swagger")
.description("설명 부분")
.version("1.0.0")
.build();
}
}
Spring Data JPA 의존성을 추가한 후에는 별도의 설정이 필요함=> 애플리케이션이 정상적으로 실행될 수 있게 연동할 데이터베이스의 정보를 application.properties에 작성해야 함
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/springboot
spring.datasource.username=flature
spring.datasource.password=aroundhub12#
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요 없음
=> 이 기능을 하는 것이 '엔티티'
- JPA에서 엔티티: 데이터베이스 테이블에 대응하는 클래스
- 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의함
- 엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있음
Product 엔티티 클래스 작성
package com.springboot.jpa.data.entity;
import java.time.LocalDateTime;
import javax.persistence.*;
import lombok.*;
@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;
}
@Entity: 해당 클래스가 엔티티임을 명시하기 위한 어노테이션
@Table: 클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우에 사용
@Id: 해당 어노테이션이 선언된 필드는 테이블의 기본값 역할로 사용됨(모든 엔티티는 @Id 어노테이션이 필요함)
@GeneratedValue: 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용됨(일반적으로 @Id 어노테이션과 함께 사용됨)
@Column: 필드에 몇 가지 설정을 더할 때 사용됨(엔티티 클래스의 필드는 자동으로 테이블 칼럼에 매핑되며 별다른 설정을 하지 않을 예정이라면 이 어노테이션을 명시하지 않아도 괜찮음)
@Transient: 엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에는 필요 없을 경우, 이 어노테이션을 사용하여 데이터베이스에서 이용하지 않게 할 수 있음
Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공함
스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용 가능
리포지토리(Repository)=> Spring Data JPA가 제공하는 인터페이스, 엔티티가 생성한 데이터베이스에 접근하는 데 사용됨
리포지토리 생성
public interface ProductRepository extends JpaRepository<Product, Long> {
}
JpaRepository 상속 구조
리포지토리에서는 몇 가지 명명규칙에 따라 커스텀 메서드도 생성할 수 있음
리포지토리에서 기본적으로 제공하는 조회 메서드는 기본값으로 단일 조회하거나 전체 엔티티를 조회하는 것만 지원하고 있기 때문에 필요에 따라 다른 조회 메서드가 필요함
메서드에 이름을 붙일 때는 첫 단어를 제외한 이후 단어들의 첫 글자를 대문자로 설정해야 JPA에서 정상적으로 인식하고 쿼리를 자동으로 만들어 줌
조회 메서드(find)에 조건으로 붙일 수 있는 몇 가지 기능들(일부) 소개
- FindBy: SQL문의 where 절 역할을 수행하는 구문, findBy 뒤에 엔티티의 필드값을 입력해서 사용함
예) findByName(String name)
- AND, OR: 조건을 여러 개 설정하기 위해 사용한다.예) findByNameAndEmail(String name, String email)
- Like/NotLike: SQL문의 like와 동일한 기능을 수행하며, 특정 문자를 포함하는지 여부를 조건으로 추가함
비슷한 키워드로 Containing, Contains, isContaing이 있음
- StartsWith/StartingWith: 특정 키워드로 시작하는 문자열 조건을 설정함
- EndsWith/EndingWith: 특정 키워드로 끝나는 문자열 조건을 설정함
- IsNull/IsNotNull: 레코드 값이 Null이거나 Null이 아닌 값을 검색함
- True/False: Boolean 타입의 레코드를 검색할 때 사용함
- Before/After: 시간을 기준으로 값을 검색함
- LessThan/GreaterThan: 특정 값(숫자)을 기준으로 대소 비교를 할 때 사용함
- Between: 두 값(숫자) 사이의 데이터를 조회함
DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체임
비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행함
DAO 클래스는 일반적으로 '인터페이스-구현체' 구성으로 생성함
DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴이며, 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있음
인터페이스 구성-> 기본적인 CRUD를 다루기 위해 인터페이스에 메서드 정의
package com.springboot.jpa.data.dao;
import com.springboot.jpa.data.entity.Product;
// 예제 6.9
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;
}
인터페이스의 구현체 생성
package com.springboot.jpa.data.dao.impl;
@Component
public class ProductDAOImpl implements ProductDAO {
private ProductRepository productRepository;
@Autowired
public ProductDAOImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Product insertProduct(Product product) {
return null;
}
@Override
public Product selectProduct(Long number) {
return null;
}
@Override
public Product updateProductName(Long number, String name) throws Exception {
return null;
}
@Override
public void deleteProduct(Long number) throws Exception {
}
}
DAO 객체<- 데이터베이스에 접근하기 위해 리포지토리 인터페이스를 사용해 의존성 주입을 받아야 함
인터페이스에 정의한 메서드 구현
insertProduct() 메서드(Product 엔티티를 데이터베이스에 저장하는 기능 수행)
@Override
public Product insertProduct(Product product) {
Product savedProduct = productRepository.save(product);
return savedProduct;
}
selectProduct() 메서드(조회 메서드)
@Override
public Product selectProduct(Long number) {
Product selectedProduct = productRepository.getById(number);
return selectedProduct;
}
getById() 메서드, findById() 메서드(단건 조회를 위한 기본 메서드, 리포지토리에서 제공)
getById(): 내부적으로 EntityManager의 getReference() 메서드 호출-> 프락시 객체 리턴-> 최초로 데이터에 접근하는 시점에 실행됨(데이터가 존재하지 않는 경우=> EntityNotFoundException 발생)
findById(): 내부적으로 EntityManager의 find() 메서드 호출, 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 실제 데이터베이스에서 데이터를 조회함, 리턴 값으로 Optional 객체 전달
updateProductName() 메서드(업데이트 메서드)
@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;
}
JPA는 값을 갱신할 때 update라는 키워드 사용x
여기서는 영속성 컨텍스트를 활용해 값을 갱신-> find() 메서드를 통해 데이터베이스에서 값을 가져오면, 가져온 객체가 영속성 컨텍스트에 추가됨,
영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행하면 JPA에서는 더티 체크라고 하는 변경 감지를 수행함
deleteProduct() 메서드(삭제 메서드)
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와 마찬가지로 추상화해서 구성, service 패키지와 클래스, 인터페이스 구성
data 패키지 안에 dto 패키지를 생성하고 ProductDto와 ProductResponseDto 클래스를 생성함
ProductDto 클래스
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;
}
}
ProductResponseDto 클래스
public class ProductResponseDto {
private String name;
private int price;
private int stock;
public ProductResponseDto(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;
}
}
ProductService 인터페이스 설계
public interface ProductService {
ProductResponseDto getProduct(Long number);
ProductResponseDto saveProduct(ProductDto productDto);
ProductResponseDto changeProductName(Long number, String name) throws Exception;
void deleteProdict(Long number) throws Exception;
}
서비스 인터페이스 구현체 클래스
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 java.time.LocalDateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@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.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);
}
}
getProduct() 메서드: 조회 메서드
saveProduct() 메서드: 저장 메서드, 상품 정보를 전달하고 애플리케이션을 거쳐 데이터베이스에 저장하는 역할 수행
changeProductName() 메서드: 업데이트 메서드, 상품정보 중 이름을 변경하는 작업 수행
deleteProduct() 메서드: 삭제 메서드, 상품정보를 삭제하는 작업 수행
컨트롤러는 클라이언트로부터 요청을 받고 해당 요청에 대해 서비스 레이어에 구현된 적절한 메서드를 호출해서 결괏값을 받음
컨트롤러는 요청과 응답을 전달하는 역할만 맡는 것이 좋음
컨트롤러 예제- ProductController 클래스
@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("정상적으로 삭제되었습니다.");
}
}
ChangeProductNameDto 클래스
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() {
}
public Long getNumber() {
return this.number;
}
public String getName() {
return this.name;
}
public void setNumber(Long number) {
this.number = number;
}
public void setName(String name) {
this.name = name;
}
}
지금까지 구현된 코드=> 상품정보를 조회, 저장, 삭제할 수 있는 기능을 비롯해 상품정보 중 상품의 이름을 수정하는 기능 포함
각 기능에 대한 요청=> '컨트롤러 - 서비스 - DAO - 리포지토리' 계층을 따라 이동, 역순으로 응답 전달
컨트롤러의 각 기능에 API 명세 작성(작성 방법은 전과 동일, basePackage를 정의하는 코드에서 패키지명을 현재 실습 중인 패키지 경로로 수정하면 됨)-> Swagger API를 사용하기 위해 애플리케이션을 실행하고 웹 브라우저를 통해 Swagger 페이지(http://localhost:8080/swagger-ui.html)로 접속
상품정보 저장: POST 메서드를 사용하는 createProduct() 메서드 사용-> 상품 이름='연필', 가격='1000', 재고수량='100'
Execute 버튼 클릭-> 하이버네이트 로그 출력
Hibernate:
insert
into
product
(created_at, name, price, stock, updated_at)
values
(?, ?, ?, ?, ?)
정상적으로 insert 쿼리가 생성되어 실행됨-> 실제로 데이터베이스에 데이터가 저장됐는지 HeidisSQL을 통해 데이터베이스 확인
createProduct 명령어를 최초로 실행한 상태면 number 칼럼의 값은 1로 나옴
Swagger에서 입력한 이름과 가격, 재고수량이 정상적으로 입력되고 ProductService에서 구현했던 saveProduct() 메서드를 통해 created_at과 updated_at 칼럼에 시간이 포함된 데이터가 추가됨
-> Swagger 페이지에서 데이터베이스에서 확인한 값 입력-> Execute 버튼-> 결과 페이지에서 응답 Body에 값이 담긴 것 확인
updateProductName() 메서드를 통해 상품의 이름 변경
{
"name": "지우개",
"number": 4
}
Swagger 페이지에 위와 같이 Body 값에 식별자 번호를 입력하고 바꾸고자 하는 이름 기입-> Execute 버튼-> 이름이 변경된 상태의 값이 응답으로 옴
상품정보 삭제
데이터베이스에서 확인했던 number값을 파라미터에 입력-> Execute 버튼-> 컨트롤러에서 deleteProduct() 메서드를 작성할 때 정상적으로 삭제가 되면 문자열 값을 Body 값에 담아 전달하도록 구현했기 때문에 'Response Body' 부분에 삭제 확인 메시지가 담겨 있음
롬복(Lombok)은 데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리임
롬복 활용의 장점
- 어노테이션 기반으로 코드를 자동 생성하므로 생산성이 높아짐
- 반복되는 코드를 생략할 수 있어 가독성이 좋아짐
- 롬복을 안다면 간단하게 코드를 유추할 수 있어 유지보수에 용이함
pom.xml 롬복 의존성 추가
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependencies>
인텔리제이 IDEA 메뉴 [File]-> [Settings]-> [Plugins]-> Marketplace에서 'lombock'을 검색해 설치
[Settings]-> [Build, Execution, Deployment]-> [Compiler]-> [Annotation Processors]-> [Enable annotation processing] 항목 체크-> [OK]
Product 엔티티 클래스에 롬복 적용
@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;
}
롬복의 어노테이션이 실제로 어떤 메서드를 생성하는지 인텔리제이 IDEA에서 확인 가능
Product 클래스에 마우스 오른쪽 버튼-> [Refactor]-> [Delombok]-> [All Lombok annotations]: 롬복의 어노테이션이 실제 코드로 리팩토링됨(코드 확인 후 Ctrl+Z 사용하여 롬복 적용 상태로 되돌려야 함)
Delombok을 하지 않고 생성된 메서드가 어떤 종류가 있는지 확인하려면 어노테이션을 작성하고 인텔리제이 IDEA 좌측의 [Structure]을 클릭=> 해당 클래스에 정의된 메서드의 목록 확인 가능
Q1(드래그 하여 정답 확인)
1. Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요 없다. 이 기능을 가능하게 하는 것은 무엇인가?
답: 엔티티
2. 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체는 무엇인가?
답: DAO(Data Access Object)
3. 엔티티 관련 기본 어노테이션 중 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용되며 일반적으로 @Id 어노테이션과 함께 사용되는 것은 무엇인가?
답: @GeneratedValue
4. 데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리는 무엇인가?
답: 롬복(Lombok)
5. Spring Data JPA가 제공하는 인터페이스, 엔티티가 생성한 데이터베이스에 접근하는 데 사용되는 것은 무엇인가?
답: 리포지토리(Repository)
6. 조회 메서드(find)에 조건으로 붙일 수 있는 기능들 중 SQL문의 where 절 역할을 수행하는 구문으로, 뒤에 엔티티의 필드값을 입력해서 사용하는 것은? ex) (정답)Name(String name)
답: FindBy
7. 다음 중 롬복 활용의 장점이 아닌 것은?
(a) 어노테이션 기반으로 코드를 자동 생성하므로 생산성이 높아짐
(b) 반복되는 코드를 생략할 수 있어 가독성이 좋아짐
(c) 롬복을 안다면 간단하게 코드를 유추할 수 있어 유지보수에 용이함
(d) 모든 IDE와 환경에서 아무 설정 없이 즉시 사용할 수 있음
답: (d)
Q2
1. 리포지토리를 생성하는 코드이다. '//1번//' 부분에 작성해야 할 코드를 작성하시오. (정답은 교재의 코드를 기준으로 함)
public interface ProductRepository extends //1번// {
}
답: JpaRepository<Product, Long>
2. 다음은 컨트롤러 생성 예제 중 ProductController 클래스의 코드이다. '//2번//' 부분에 들어갈 코드를 작성하시오.
--생략--
public class ProductController {
--생략--
@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);
}
//2번//
public ResponseEntity<String> deleteProduct(Long number) throws Exception {
productService.deleteProduct(number);
return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
}
}
답: @DeleteMapping()
출처: 스프링부트 핵심 가이드
SPRING #2
Editor : 농담곰
[스프링 2] 7장. 테스트코드 작성하기 (0) | 2024.12.27 |
---|---|
[스프링 2팀]5~6장. API를 작성하는 다양한 방법 & 데이터베이스 연동 (0) | 2024.11.22 |
[스프링 2팀] 1~4장. 스프링 부트 개발 환경과 애플리케이션 개발하기 (0) | 2024.11.15 |
[스프링 2팀] 스프링 입문 - 섹션 07~08 (0) | 2024.11.08 |
[스프링 2팀] 스프링 입문 - 섹션 05~06 (0) | 2024.10.11 |