Object Relational Mapping: 객체 관계 매핑
이 둘의 불일치와 제약사항을 해결하는게 바로 ORM
ORM 장점
ORM 단점
member.getOrganization().getAddress()
와 같이 접근할 수 있음JPA의 메커니즘
JPA 기반의 구현체 3가지
Hibernate
Spirng Data JPA
애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능 수행
엔티티 객체가 영속성 컨텍스트에 들어오면 → JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업 수행
엔티티 객체가 영속성 컨텍스트에 들어와 JPA의 관리 대상이 되는 시점부터는 해당 객체를 영속 객체(Persistence Object)라고 부름
애플리케이션과 데이터베이스와의 관계
영속성 컨텍스트의 특징
SimpleJpaRepository
가 레포지터리에서 엔티티 매니저를 사용하는 것을 알 수 있음.// SimpleJpaRepository의 EntityManager 의존성 주입 코드
public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManagerentityManager) {
Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
Assert.notNull(entityManager, "EntityManager must not be null!");
this.entityInformation = entityInformation;
this.em = entityManager;
this.provider = PersistenceProvider.fromEntityManager(entityManager);
}
application.properties
에서 작성한 최소한의 설정만으로도 동작하지만, JPA의 구현체 중 하나인 하이버네이트에서는 persistaence.xml
이라는 설정 파일을 구성하고 사용해야 하는 객체// 엔티티 매니저 팩토리 사용을 위한 persistence.xml 파일 설정
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1/xsd"
version="2.1">
<persistence=unit name="entity_manager_factory" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="password" />
<property name="javax.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/springboot" />
<property name="hibernate.dialect" value="org.hibernate.dialect.MariaDB103Dialect" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
</properties>
</persistence-unit>
</persistence>
📣 Trobleshooting 참고 링크
...
public Docket restAPI() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("groupName1")
.select()
.apis(RequestHandlerSelectors.
basePackage("com.springboot.jpa"))
...
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password={비밀번호}
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
...
//MySQL
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//Lombok
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
...
연동 되었는지 확인
첫 실행 시 데이터베이스를 MysqlWorkbench에서 생성했음에도 다음과 같은 오류 발생 가능
터미널 환경에서 접속해서 만들어주면 해결됨.
재실행 시 잘 실행되는 모습
📣 참고1: MySQL 실행
mysql.server start
- 종료 시 :
mysql.server stop
📣 참고2: properties 환경변수 설정
환경변수를 넣는 부분이 안보인다면?
- Modify options > Environment variables 설정해주기!
- 환경변수 내용 작성 및 저장
📣 참고3: 설정파일 ignore
5가지 분류
운영 환경에서는 create, create-drop, update 기능은 사용하지 않음.
개발 환경에서는 create 또는 update를 사용하는 편
show-sql
Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없음. → 이 기능을 가능하게 하는 것이 엔티티
JPA에서의 엔티티
엔티티 클래스로 구현하기
data.entity
패키지를 생성하고 그 안에 Product
엔티티 클래스를 생성
data/entity/Product.java
package com.springboot.api.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;
}
위와 같이 클래스를 생성하고 application.properties
에 정의한 spring.jpa.hibernate.ddl-auto
의 값을 create
같은 테이블을 생성하는 옵션으로 설정 → 쿼리문을 작성하지 않아도 데이터베이스에 테이블이 자동으로 만들어짐.
엔티티 작성 시 어노테이션을 많이 사용!
@Entity
@Table
@Table
어노테이션이 필요하지 않음.@Table
어노테이션을 사용할 때는 클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우임.@Table(name = 값)
형태로 데이터베이스의 테이블명 명시@Id
@GeneraatedValue
일반적으로 @Id
어노테이션과 함께 사용됨.
이 어노테이션은 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용됨.
값 생성 방식
GeneratedValue를 사용하지 않는 방식(=직접 할당)
- 애플리케이션에서 자체적으로 고유한 기본값을 생성할 경우 사용하는 방식
- 내부에 정해진 규칙에 의해 기본값을 생성하고 식별자로 사용됨
AUTO
@GeneratedValue
의 기본 설정 값- 기본값을 사용하는 데이터베이스에 맞게 자동 생성
IDENTITY
- 기본값 생성을 데이터베이스에 위임하는 방식
- 데이터베이스의
AUTO_INCREMENT
를 사용해 기본값을 생성SEQUENCE
@SequenceGenerator
어노테이션으로 식별자 생성기를 설정하고 이를 통해 값을 자동 주입 받음.@SequenceGenerator
를 정의할 때는 name, sequencceName, allocationSize를 활용@GeneratedValue
에 생서기를 설정TABLE
- 어떤 DBMS를 사용하더라도 동일하게 동작하기를 원할 경우 사용
- 식별자로 사용할 숫자의 보관 테이블을 별도로 생성해서 엔티티를 생성할 때마다 값을 갱신해 사용
@TableGenerator
어노테이션으로 테이블 정보 설정
@Column
@Column
어노테이션은 필드에 몇 가지 설정을 더할 때 사용함.// Column 어노테이션의 요소 목록
public @interface Column {
String name() default "";
boolean unique() default false;
boolean nullable() default true;
boolean insertable() default true;
boolean updatetable() default true;
String columnDefinition() default "";
String table() default "";
int length() default 255;
int precision() default 0;
int scale() default 0;
}
@Transient
JpaRepository
를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공JpaRepository
를 상속하는 인터페이스 생성 → 기존의 다양한 메서드 손쉽게 활용 가능여기에서의 Repository : Spring Data JPA가 제공하는 인터페이스
엔티티를 데이터베이스의 테이블과 구조를 생성하는 데 사용했다면 리포지토리는 엔티티가 생성한 테이터베이스에 접근하는 데 사용됨
Repository 생성
JpaRepository
상속받기ProductRepository
가 JpaRepository
상속받을 때는 대상 엔티티와 기본값 타입을 지정해야 함.Product
로 설정하고, 해당 엔티티의 @Id
필드 타입인 Long을 설정package com.springboot.api.data.repository;
import com.springboot.api.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
생성된 Repository는 JpaRepository
를 상속받으면서 별도의 메서드 구현 없이도 많은 기능 제공
JpaRepository
의 상속 구조ProductRepository
에서도 사용 가능조회 메서드(find)에 조건으로 붙일 수 있는 몇 가지 기능
- FindBy
- SQL문의 where절 역할을 수행하는 구문
- findBy 뒤에 엔티티의 필드값을 입력해 사용
- ex) findByName(String name)
- AND, OR
- 조건을 여러 개 설정하기 위해 사용
- ex) findBynameAndEmail(Stirng 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 객체가 수행
다만, 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체
규모가 작은 서비스
📣 DAO vs Repository
- Repository는 Spring Data JPA에서 제공하는 기능이기 때문에 기존의 스프링 프레임워크나 스프링 MVC의 사용자는
Repostory 대신 DAO 객체로 데이터베이스에 접근
함.- 이러한 측면에서 각 컴포넌트의 역할을 고민하는 시간을 가지면 좋을 것 같음.
package com.springboot.api.data.dao;
import com.springboot.api.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;
};
일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달
인터페이스 구현체 클래스 작성
📁 data/dao/impl/ProductDAOImpl.java
package com.springboot.api.data.dao.impl;
import com.springboot.api.data.dao.ProductDAO;
import com.springboot.api.data.entity.Product;
import com.springboot.api.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 {
private final ProductRepository productRepository;
@Autowired
public ProductDAOImpl(ProductRepository productRepository){
this.productRepository = productRepository;
}
//Product 엔티티를 데이터베이스에 저장하는 기능 수행
@Override
public Product insertProduct(Product product){
Product savedProduct = productRepository.save(product);
return savedProduct;
}
//조회
@Override
public Product selectProduct(Long number){
Product selectProduct = productRepository.getById(number);
return selectProduct;
}
//업데이트
@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();
}
}
}
getById()
- 내부적으로 EntityManager와 getReference() 메서드를 호출함.
- getReference() 메서드를 호출하면 프록시 객체를 리턴
- 실제 쿼리는 프록시 객체를 통해 최초로 데이터에 접근하는 시점에 실행
- 이때 데이터가 존재하지 않는 경우 → EntityNotFoundException이 발생
- JpaRepository의 실제 구현체인 SimpleJpaRepository의 getById() 메서드
//SimpleJpaRepository의 getById() 메서드 @Override public T getById(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); return em.getReference(getDomainClass(), id); }
findById()
- 내부적으로 EntityManager의 find() 메서드를 호출
- 이 메서드는 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 → 실제 데이터베이스에서 데이터를 조회
- 리턴 값으로 Optional 객체를 전달
// SimpleJpaRepository의 findById() 메서드 @Override public Optional<T> findById(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); Class<T> domainType = getDomainClass(); if (metadata == null) { return Optional.ofNullable(em.find(domainType, id)); } LockModeType type = metadata.getLockModeType(); Map<String, Object> hints = new Hashmap<>(); getQueryHints().withFetchGraphs(em).forEach(hints::put); return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints)); }
JPA에서 데이터의 값을 변경할 때는 다른 메서드와는 다른 점이 있음
- JPA : 값을 갱신할 때 update라는 키워드 사용 X → 영속성 컨텍스트를 활용해 값을 갱신
- find() 메서드를 통해 데이터베이스에서 값을 가져오면 가져온 객체가 영속성 컨텍스트에 추가됨.
- 영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행 → JPA에서는 Dirty Check라고 하는 변경 감지를 수행함.
@Transactional
어노테이션이 지정돼 있으면 → 메서드 내 작업을 마칠 경우 자동으로flush()
메서드를 실행함.- 이 과정에서 변경이 감지되면 → 대상 객체에 해당하는 데이터베이스의 레코드를 업데이트하는 쿼리가 실행됨.
//SimpleJpaRepository의 save() 메서드 @Transactional @Override public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null."); if(entityInformation.isNew(entity) { em.persist(entity); return entity; } else { return em.merge(entity); } }
데이터베이스의 레코드를 삭제하기 위해서는 삭제하고자 하는 레코드와 매핑된 영속 객체를 영속성 컨텍스트에 가져와야 함.
deleteProduct()
메서드는findById()
메서드를 통해 객체를 가져오는 작업을 수행하고delete()
메서드를 통해 해당 객체의 삭제 요청을 함.
delete()
메서드로 전달받은 엔티티가 영속성 컨ㅌ텍스트에 있는지 파악하고, 해당 엔티티를 영속성 컨텍스트에 영속화하는 작업을 거쳐 데이터베이스의 레코드와 매핑- 매핑된 영속 객체를 대상으로 삭제 요청을 수행하는 메서드를 실행해 작업을 마치고 commit 단계에서 삭제를 진행
// SimpleJpaRepository의 delete() 메서드 @Override @Transactional @SuppressWarnings("unchecked") public void delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); if(entityInformation.isNew(entity) { return; } Class<?> type = ProxyUtils.getUserClass(entity); T existing = (T) em.find(type, entityInformation.getId(entity)); //if the entity to be deleted doesn't exist, delete is a NOOP if(existing == null) { return; } em.remove(em.contains(entity) ? entity : em.merge(entity);
서비스 레이어에서는 도메인 모델(Domain Model)을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공
서비스 인터페이스를 작성하기 전에 필요한 DTO 클래스 생성하기
package com.springboot.api.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;
}
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 stork){
this.stock = stock;
}
}
package com.springboot.api.data.dto;
public class ProductResponseDTO {
private Long number;
private String name;
private int price;
private int stock;
public ProductResponseDTO(Long number, String name, int price, int stock){
this.number = number;
this.name = name;
this.price = price;
this.stock = 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.api.service;
import com.springboot.api.data.dto.ProductDTO;
import com.springboot.api.data.dto.ProductResponseDTO;
import com.springboot.api.data.entity.Product;
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;
}
스프링부트 애플리케이션의 구조
서비스 인터페이스 구현체 클래스
package com.springboot.api.service.Impl;
import com.springboot.api.data.dao.ProductDAO;
import com.springboot.api.data.dto.ProductDTO;
import com.springboot.api.data.dto.ProductResponseDTO;
import com.springboot.api.service.ProductService;
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){
return null;
}
@Override
public ProductResponseDTO saveProduct(ProductDTO productDTO){
return null;
}
@Override
public ProductResponseDTO changeProductName(Long number, String name) throws Exception {
return null;
}
@Override
public void deleteProduct(Long number) throws Exception {
}
}
'ProductResponseDTO(java.lang.Long, java.lang.String, int, int)' in 'com.springboot.api.data.dto.ProductResponseDTO' cannot be applied to '()’
package com.springboot.api.service.Impl;
import com.springboot.api.data.dao.ProductDAO;
import com.springboot.api.data.dto.ProductDTO;
import com.springboot.api.data.dto.ProductResponseDTO;
import com.springboot.api.data.entity.Product;
import com.springboot.api.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;
}
// getProduct() 메서드 구현
@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);
}
}
코드 수정
set 메서드를 사용하지 않고, 기존 생성자를 이용해서 ProductResponseDTO 객체 생성
package com.springboot.api.service.Impl;
import com.springboot.api.data.dao.ProductDAO;
import com.springboot.api.data.dto.ProductDTO;
import com.springboot.api.data.dto.ProductResponseDTO;
import com.springboot.api.data.entity.Product;
import com.springboot.api.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;
}
// getProduct() 메서드 구현
@Override
public ProductResponseDTO getProduct(Long number){
Product product = productDAO.selectProduct(number);
return new ProductResponseDTO(product.getNumber(), product.getName(), product.getPrice(), product.getStock());
}
@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);
return new ProductResponseDTO(savedProduct.getNumber(), savedProduct.getName(), savedProduct.getPrice(), savedProduct.getStock());
}
@Override
public ProductResponseDTO changeProductName(Long number, String name) throws Exception {
Product changedProduct = productDAO.updateProductName(number, name);
return new ProductResponseDTO(changedProduct.getNumber(), changedProduct.getName(), changedProduct.getPrice(), changedProduct.getStock());
}
@Override
public void deleteProduct(Long number) throws Exception {
productDAO.deleteProduct(number);
}
}
package com.springboot.api.controller;
import com.springboot.api.data.dto.ChangeProductNameDTO;
import com.springboot.api.data.dto.ProductDTO;
import com.springboot.api.data.dto.ProductResponseDTO;
import com.springboot.api.data.repository.ProductRepository;
import com.springboot.api.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(produces = "text/plain;charset=UTF-8")
public ResponseEntity<String> deleteProduct(Long number) throws Exception {
productService.deleteProduct(number);
return ResponseEntity.status(HttpStatus.OK).body("정상적으로 삭제되었습니다.");
}
}
Post API
error
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.springboot.api.data.dto.ProductDTO (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 3]
해결
- ProductDTO의 기본 생성자 지정
```java
package com.springboot.api.data.dto;
public class ProductDTO {
private String name;
private int price;
private int stock;
public ProductDTO(){
}
...
```
GET API
PUT API
DELETE API
DeleteMapping에 produces로 UTF-8 설정해주기
// ProductController.java
...
@DeleteMapping(produces = "text/plain;charset=UTF-8")
...
데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리
자바에서 데이터 클래스를 작성하면 대게 많은 멤버 변수를 선언
각 멤버 변수별로 getter/setter 메서드를 만듦 → 코드가 길어지고 가독성 down
Lombok의 장점
Lombok의 단점
pom.xml(build.gradle) 파일에 다음 코드가 추가되어 있는지 확인
<dependencies>
...
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
...
</dependencies>
...
dependencies {
//Lombok
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
...
}
...
Lombok 플러그인 설치
settings > Plugins에서 Lombok 검색 후 설치
package com.springboot.api.data.entity;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString(exclude = "name")
@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;
}
적용 확인 방법
Refactor > Delombok > All Lombok annotations
package com.springboot.api.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;
public Product(Long number, String name, Integer price, Integer stock, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Product() {
}
public Long getNumber() {
return this.number;
}
public String getName() {
return this.name;
}
public Integer getPrice() {
return this.price;
}
public Integer getStock() {
return this.stock;
}
public LocalDateTime getCreatedAt() {
return this.createdAt;
}
public LocalDateTime getUpdatedAt() {
return this.updatedAt;
}
public void setNumber(Long number) {
this.number = number;
}
public void setName(String name) {
this.name = name;
}
public void setPrice(Integer price) {
this.price = price;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof Product)) return false;
final Product other = (Product) o;
if (!other.canEqual((Object) this)) return false;
final Object this$number = this.getNumber();
final Object other$number = other.getNumber();
if (this$number == null ? other$number != null : !this$number.equals(other$number)) return false;
final Object this$name = this.getName();
final Object other$name = other.getName();
if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
final Object this$price = this.getPrice();
final Object other$price = other.getPrice();
if (this$price == null ? other$price != null : !this$price.equals(other$price)) return false;
final Object this$stock = this.getStock();
final Object other$stock = other.getStock();
if (this$stock == null ? other$stock != null : !this$stock.equals(other$stock)) return false;
final Object this$createdAt = this.getCreatedAt();
final Object other$createdAt = other.getCreatedAt();
if (this$createdAt == null ? other$createdAt != null : !this$createdAt.equals(other$createdAt)) return false;
final Object this$updatedAt = this.getUpdatedAt();
final Object other$updatedAt = other.getUpdatedAt();
if (this$updatedAt == null ? other$updatedAt != null : !this$updatedAt.equals(other$updatedAt)) return false;
return true;
}
protected boolean canEqual(final Object other) {
return other instanceof Product;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $number = this.getNumber();
result = result * PRIME + ($number == null ? 43 : $number.hashCode());
final Object $name = this.getName();
result = result * PRIME + ($name == null ? 43 : $name.hashCode());
final Object $price = this.getPrice();
result = result * PRIME + ($price == null ? 43 : $price.hashCode());
final Object $stock = this.getStock();
result = result * PRIME + ($stock == null ? 43 : $stock.hashCode());
final Object $createdAt = this.getCreatedAt();
result = result * PRIME + ($createdAt == null ? 43 : $createdAt.hashCode());
final Object $updatedAt = this.getUpdatedAt();
result = result * PRIME + ($updatedAt == null ? 43 : $updatedAt.hashCode());
return result;
}
public String toString() {
return "Product(number=" + this.getNumber() + ", price=" + this.getPrice() + ", stock=" + this.getStock() + ", createdAt=" + this.getCreatedAt() + ", updatedAt=" + this.getUpdatedAt() + ")";
}
}
다시 적용 상태로 되돌리기
@Getter
, @Setter
public Long getNumber() {
return this.number;
}
public String getName() {
return this.name;
}
public Integer getPrice() {
return this.price;
}
public Integer getStock() {
return this.stock;
}
public LocalDateTime getCreatedAt() {
return this.createdAt;
}
public LocalDateTime getUpdatedAt() {
return this.updatedAt;
}
public void setNumber(Long number) {
this.number = number;
}
public void setName(String name) {
this.name = name;
}
public void setPrice(Integer price) {
this.price = price;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
생성자 자동 생성 어노테이션
데이터 클래스 초기화를 위한 생성자를 자동으로 만들어주는 어노테이션 3가지
NoArgsConstructor
AllArgsConstructor
RequiredArgsConstructor
현재 Product 클래스에는 @RequiredArgsConstructor로 정의될 필드가 없기 때문에 다른 두 개의 어노테이션을 Delombok해서 나온 코드
public Product(Long number, String name, Integer price, Integer stock, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Product() {
}
@ToString
public String toString() {
return "Product(number=" + this.getNumber() + ", name=" + this.getName() + ", price="
+ this.getPrice() + ", stock=" + this.getStock() + ", createdAt=" this.getCreatedAt()
+", updatedAt=" + this.getUpdatedAt() + ")";
}
@ToString 어노테이션의 exclude 속성 활용
@ToString(exclude = "name")
@Table(name = "product")
public class Product {
...(생략)
}
@EqualsAndHashCode
equals : 두 객체의 내용이 같은지 동등성을 비교
hashcode : 두 객체가 같은 객체인지 동일성을 비교
public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof Product)) return false;
final Product other = (Product) o;
if (!other.canEqual((Object) this)) return false;
final Object this$number = this.getNumber();
final Object other$number = other.getNumber();
if (this$number == null ? other$number != null : !this$number.equals(other$number)) return false;
final Object this$name = this.getName();
final Object other$name = other.getName();
if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
final Object this$price = this.getPrice();
final Object other$price = other.getPrice();
if (this$price == null ? other$price != null : !this$price.equals(other$price)) return false;
final Object this$stock = this.getStock();
final Object other$stock = other.getStock();
if (this$stock == null ? other$stock != null : !this$stock.equals(other$stock)) return false;
final Object this$createdAt = this.getCreatedAt();
final Object other$createdAt = other.getCreatedAt();
if (this$createdAt == null ? other$createdAt != null : !this$createdAt.equals(other$createdAt)) return false;
final Object this$updatedAt = this.getUpdatedAt();
final Object other$updatedAt = other.getUpdatedAt();
if (this$updatedAt == null ? other$updatedAt != null : !this$updatedAt.equals(other$updatedAt)) return false;
return true;
}
protected boolean canEqual(final Object other) {
return other instanceof Product;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $number = this.getNumber();
result = result * PRIME + ($number == null ? 43 : $number.hashCode());
final Object $name = this.getName();
result = result * PRIME + ($name == null ? 43 : $name.hashCode());
final Object $price = this.getPrice();
result = result * PRIME + ($price == null ? 43 : $price.hashCode());
final Object $stock = this.getStock();
result = result * PRIME + ($stock == null ? 43 : $stock.hashCode());
final Object $createdAt = this.getCreatedAt();
result = result * PRIME + ($createdAt == null ? 43 : $createdAt.hashCode());
final Object $updatedAt = this.getUpdatedAt();
result = result * PRIME + ($updatedAt == null ? 43 : $updatedAt.hashCode());
return result;
}
부모 클래스가 있어 상속을 받는 상황이라면?
callSuper 기본값은 false, true일 경우 부모 객체의 값도 비교 대상에 포함
@Entity
@EqualsAndHashCode(callSuper = true)
@Table(name = "product")
public class Product entends BaseEntity {
..(생략)
}
@Data