| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 스프링부트
- spring boot
- JPA
- LinkedList
- 핵심가이드
- error
- QueryDSL
- 제로베이스
- SpringBoot
- spring
- Java
- JWT
- ORM
- 연관관계
- array
- 개발자
- 자료구조
- actuator
- 프로그래머스
- SpringSecurity
- 컴퓨터 공학
- TestCode
- 백엔드스쿨
- 서버
- 백엔드
- queue
- 백엔드공부
- 컴퓨터 구조
- restTemplate
- 백준
- Today
- Total
be_better
[Springboot] 데이터베이스 연동 - 프로젝트 적용 본문
목차
데이터베이스 연동
프로젝트 생성
MySQL에 로 jpa 이름의 database를 생성하고 사용하도록 해준다.
create database jpa;
use jpa;
프로젝트는 다음과 같이 만들어준다.


폴더명과 프로젝트명은 자유롭게 설정
application.yml 파일에 mysql Driver 설정
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/jpa?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: password
jpa:
show-sql: true
database: mysql
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
format_sql: true
com.mysql.cj.jdbc.Driver를 찾지 못하고 빨간줄로 되어있다면
MySQL의 버전을 확인 후 build.gradle 파일에 다음을 추가해준다.
dependencies {
runtimeOnly 'mysql:mysql-connector-java:8.0.28'
}
jpa.hibernate.ddl-auto는 데이터베이스를 자동으로 조작하는 옵션이다. 사용할 수 있는 옵션은 다음과 같다.
- create: 애플리케이션이 가동되고 SessionFactory가 실행될 때 기존 테이블을 지우고 새로 생성한다.
- create-drop: create와 동일한 기능을 수행하나 애플리케이션을 종료하는 시점에 테이블을 지운다.
- update: SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마를 갱신한다. 기존에 저장된 데이터는 유지된다.
- validate: update처럼 객체를 검사하지만 스키마는 건드리지 않는다. 검사 과정에서 데이터베이스의 테이블 정보과 객체의 정보가 다르면 에러를 발생한다.
- none: ddl-auto 기능을 사용하지 않는다.
운영 환경에서는 create, create-drop, update 기능은 축적된 데이터를 지워버릴 수도 있고, 사람의 실수로 객체의 정보가 변경됐을 때 운영 환경의 데이터베이스 정보까지 변경될 수 있기 때문에 사용하지 않고 대체로 validate나 none을 사용한다.
jpa.show-sql은 로그에 하이버네이트가 생성한 쿼리문을 출력하는 옵션이다. 아무 설정이 없으면 저장에 용이한 형태로 출력되기 때문에 사람이 보기에는 불편하게 한 줄로 출력된다.
jpa.properties.hibernate.format_sql 옵션으로 사람이 보기 좋게 포매팅할 수 있다.
엔티티 설계
Spring Data JPA를 사용하면 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없다. 이 기능을 가능하게 하는 것이 엔티티(Entity)이다. JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의한다. 엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있다.
data.entity 패키지를 생성하고 Product Entity class를 생성한다.
package com.springboot.jpa.data.entity;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@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 클래스를 생성하면 쿼리문을 작성하지 않아도 데이터베이스에 테이블이 자동으로 만들어진다.


엔티티 관련 기본 어노테이션
엔티티를 작성할 때는 어노테이션을 많이 사용한다.
@Entity
해당 클래스가 엔티티임을 명시하기 위한 어노테이션으로 클래스 자체는 테이블과 일대일로 매칭되며, 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미한다.
@Table
엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 @Table 어노테이션이 필요하지 않는다. @Table 어노테이션을 사용할 때는 클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우로 서로 다른 이름을 쓰려면 @Table(name = 값) 형태로 데이터베이스의 테이블명을 명시해야 한다. 대체로 자바의 명명법과 데이터베이스가 사용하는 명명법이 다르기 때문에 자주 사용된다.
@Id
엔티티 클래스의 필드는 케이블의 칼럼과 매핑되는데 @Id 어노테이션이 선언된 필드는 테이블의 기본값 역할로 사용되므로 모든 엔티티는 @Id 어노테이션이 필요하다.
@GeneratedValue
일반적으로 @Id 어노테이션과 함께 사용되는 어노테이션으로 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용한다.
값 생성 방식
- GeneratedValue를 사용하지 않는 방식(직접 할당)
- 애플리케이션에서 자체적으로 고유한 기본값을 생성할 경우 사용하는 방식
- 내부에 정해진 규칙에 의해 기본값을 생성하고 식별자로 사용한다.
- AUTO
- @GeneratedValue의 기본 설정값
- 기본값을 사용하는 데이터베이스에 맞게 자동 생성한다.
- IDENTITY
- 기본값 생성을 데이터베이스에 위임하는 방식
- 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성한다.
- SEQUENCE
- @SequenceGenerator 어노테이션으로 식별자 생성기를 설정하고 이를 통해 값을 자동 주입받는다.
- SequenceGenerator를 정의할 때는 name, sequenceName, allocationSize를 활용한다.
- @GeneratedValue에 생성기를 설정한다.
- TABLE
- 어떤 DBMS를 사용하더라도 동일하게 동작하기를 원할 경우 사용한다.
- 식별자로 사용할 숫자의 보관 테이블을 별도로 생성해서 엔티티를 생성할 때마다 값을 갱신하며 사용한다.
- @TableGenerator 어노테이션으로 테이블 정보를 설정한다.
@Column
엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑된다. 별다른 설정을 하지 않을 예정이라면 이 어노테이션을 명시하지 않아도 괜찮다.
public @interface Column {
String name() default "";
boolean unique() default false;
boolean nullable() default true;
boolean insertable() default true;
boolean updatable() default true;
String columnDefinition() default "";
String table() default "";
int length() default 255;
int precision() default 0;
int scale() default 0;
}
@Column 어노테이션에서 많이 사용하는 요소
- name: 데이터베이스의 칼럼명을 설정하는 속성. 명시하지 않으면 필드명으로 지정된다.
- nullable: 레코드를 생성할 때 칼럼 값에 null 처리가 가능한지를 명시하는 속성
- length: 데이터베이스에 저장하는 데이터의 최대 길이를 설정
- unique: 해당 칼럼을 유니크로 설정
@Transient
엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서는 필요 없을 경우 이 어노테이션을 사용해 데이터베이스에서 이용하지 않게 할 수 있다.
리포지토리 인터페이스 설계
Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공한다. 스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.
리포지토리 인터페이스 생성
Repository를 생성하기 위해서는 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스를 생성하고 JpaRepository를 상속받으면 된다.
package com.springboot.jpa.data.repository;
import com.springboot.jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
ProductRepository가 JpaRepository를 상속받을 때는 대상 엔티티와 기본값 타입을 지정해야 한다. 엔티티를 사용하기 위해서는 위와 같이 대상 엔티티를 Product로 설정하고 해당 엔티티의 @Id 필드 타입인 Long을 설정하면 된다.
JpaRepository에서 제공하는 기본 메서드
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>,
QueryByExampleExecutor<T> {
@Override
List<T> findAll();
@Override
List<T> findAll(Sort sort);
@Override
List<T> findAllById(Iterable<ID> ids);
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
@Deprecated
default void deleteInBatch(Iterable<T> entities) {
deleteAllInBatch(entities);
void deleteAllInBatch(Iterable<T> entities);
void deleteAllByIdInBatch(Iterable<ID> ids);
void deleteAllInBatch();
@Deprecated
T getOne(ID id);
@Deprecated
T getById(ID id);
T getReferenceById(ID id);
@Override
<S extends T> List<S> findAll(Example<S> example);
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
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: 두 값(숫자) 사이의 데이터를 조회한다.
- OrderBy: SQL 문에서 order by 와 동일한 기능을 수행한다. 예를 들어, 가격순으로 이름 조회를 수행한다면
List<Product> findByNameOrderByPriceAsc(String name); 와 같이 작성 - countBy: SQL 문의 count와 동일한 기능을 수행하며, 결과값의 개수(count)를 추출한다.
DAO 설계
DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하는 객체이다. 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행한다. 다만 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있다.
규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비르 레이어에서 데이터베이스에 접근해서 구현하기도 하지만 실제로 업무에 필요한 비즈니스 로직을 개발하다 보면 서비스 레이어와 리포지토리의 중간 계층을 두는 것이 유지보수 측면에서 용이한 경우가 많다.
DAO 클래스 생성
DAO 클래스는 일반적으로 '인터페이스-구현체' 구성으로 생성한다. DAO 클래스는 의존성 결합을 낮추기 위한 디자인 패턴이며, 서비스 레이어에 DAO 객체를 주입받을 때 인터페이스를 선언하는 방식으로 구성할 수 있다.
프로젝트에 data.dao.impl 구조로 패키지를 생성한 후 dao 패키지와 impl 패키지에 각각 ProductDAO 인터페이스와 ProductDAOImpl 클래스를 생성한다.

ProductDAO 인터페이스
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;
}
일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달한다. 데이터 객체를 엔티티 객체로 전달할지, DTO 객체로 전달할지에 대해서는 개발자마다 의견이 분분하다. 일반적인 설계 원칙에서 엔티티 객체는 데이터베이스에 접근하는 계층에서만 사용하도록 정의하고 다른 계층으로 데이터를 전달할 때는 DTO 객체를 사용한다.
ProductDAO 인터페이스의 구현체 클래스 ProductDAOImpl
@Component
public class ProductDAOImpl implements ProductDAO {
private final 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 {
}
}
ProductDAOImpl 클래스를 스프링이 관리하는 빈으로 등록하려면 @Component 또는 @Service 어노테이션을 지정해야 한다. 빈으로 등록된 객체는 다른 클래스가 인터페이스를 가지고 의존성을 주입받을 때 이 구현체를 찾아 주입하게 된다.
마찬가지로 DAO 객체에서도 데이터베이스에 접근하기 위해 리포지토리 인터페이스를 사용해 의존성 주입을 받아야 한다. 이 때 생성자를 통해 의존성 주입을 받으면 된다.
insertProduct() 메서드
@Override
public Product insertProduct(Product product) {
Product saveProduct = productRepository.save(product);
return saveProduct;
}
리포지토리를 생성할 때 인터페이스에서 따로 메서드를 구현하지 않아도 JPA에서 기본 메서드를 제공하므로 save 메서드를 활용할 수 있다.
selectProduct() 메서드
@Override
public Product selectProduct(Long number) {
Product selectedProduct = productRepository.getById(number);
return selectedProduct;
}
리포지토리에서는 단건 조회를 위해 getById(), findById() 두 가지 기본 메서드를 제공한다.
getById()
내부적으로 EntityManager의 getReperence() 메서드를 호출한다. getReperence() 메서드를 호출하면 프락시 객체를 리턴한다. 실제 쿼리는 프락치 객체를 통해 최초로 데이터에 접근하는 시점에 실행된다. 이때 데이터가 존재하지 않는 경우에는 EntityNotFoundException이 발생한다.
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));
}
조회 기능을 구현하기 위해서는 어떤 메서드를 사용하더라도 무관하다.
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라는 키워드를 사용하지 않는다. find() 메서드를 통해 가져온 객체는 영속성 컨텍스트에 추가되고 영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행하면 JPA에서는 더티 체크(Dirty Check)라고 하는 변경 감지를 수행한다.
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);
}
}
@Transactional 어노테이션은 메서드 내 작업을 마칠 경우 자동으로 flush() 메서드를 실행한다. 이 과정에서 변경이 감지되면 대상 객체에 해당하는 데이터베이스의 레코드를 업데이트하는 쿼리가 실행된다.
deleteProduct() 메서드
@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();
}
}
deleteProduct() 메서드는 findById() 메서드를 통해 객체를 가져오는 작업을 수행하고 delete() 메서드를 통해 해당 객체를 삭제하게끔 삭제 요청을 한다.
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));
}
마지막 라인에서 delete() 메서드로 전달받은 엔티티가 영속성 컨텍스트에 있는지 파악하고, 해당 엔티티를 영속성 컨텍스트에 영속화하는 작업을 거쳐 데이터베이스의 레코드와 매핑한다. 그렇게 매핑된 영속 객체를 대상으로 삭제 요청을 수행하는 메서드를 실행해 작업을 마치고 커밋(commit) 단계에서 삭제를 진행한다.
DAO 연동을 위한 컨트롤러와 서비스 설계
앞서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 한다.
서비스 클래스 만들기
서비스 레이어에서는 도메인 모델(Domain Model)을 활용한 애플리케이션에서 제공하는 핵심 기능을 제공한다. 도메인을 활용한 세부 기능들을 비즈니스 레이어의 로직에서 구현하고, 서비스 레이어에서는 기능들을 종합해서 핵심 기능을 전달하도록 구성하는 경우가 대표적이다.
서비스 객체는 DAO와 마찬가지로 추상화해서 구성한다.
data 패키지 안에 dto 패키지를 생성하고 ProductDto와 ProductResponseDto 클래스도 생성한다.

ProductDto 클래스
package com.springboot.jpa.data.dto;
import lombok.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductDto {
private String name;
private int price;
private int stock;
}
ProductResponstDto 클래스
package com.springboot.jpa.data.dto;
import lombok.*;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
}
ProductService 인터페이스 설계
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에서 구현한 기능을 서비스 인터페이스에서 호출해 결과값을 가져오는 작업을 수행하도록 설계했다. 서비스에서는 클라이언트가 요청한 데이터를 적절하게 가공해서 컨트롤러에게 넘기는 역할을 한다.
DAO 객체에서 엔티티 타입을 사용하는 것을 고려하면 서비스 레이어에서 DTO 객체와 엔티티 객체를 각 레이어에 변환해서 전달하는 역할도 수행한다고 볼 수 있다.
데이터베이스와 밀접한 관련이 있는 데이터 액세스 레이어까지는 엔티티 객체를 사용하고, 클라이언트와 가까워지는 다른 레이어에서는 데이터를 교환하는 데 DTO 객체를 사용하는 것이 일반적이다.

서비스 인터페이스 구현체 ProductServiceImpl 클래스
@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 {
}
}
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;
}
setter을 이용해 값을 하나씩 넣어주는 작업은 Builder 패턴을 활용하거나 DTO 객체 내부에 변환하는 메서드를 추가해서 간단하게 전환할 수 있다.
saveProduct() 메서드 구현
@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;
}
전달받은 DTO 객체를 통해 엔티티 객체를 생성해서 초기화한 후 DAO 객체로 전달한다. 다만 저장 메서드의 리턴 타입을 어떻게 지정할지는 고민해야 한다. 일반적으로 저장 메서드는 void 타입으로 작성하거나 작업의 성공 여부를 나타내는 boolean 타입으로 지정하는 경우가 많다.
changeProductName() 메서드 구현
@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;
}
실제 레코드 값을 변경하는 작업은 DAO에서 진행하기 때문에 서비스 레이어에서는 해당 메서드를 호출해서 결과값만 받아와 리턴한다.
deleteProduct() 메서드 구현
@Override
public void deleteProduct(Long number) throws Exception {
productDAO.deleteProduct(number);
}
마찬가지로 DAO에서 작업을 수행하기 때문에 호출만 한다.
컨트롤러 생성
서비스 객체의 설계를 마친 후에는 비즈니스 로직과 클라이언트의 요청을 연결하는 컨트롤러를 생성해야 한다.

ProductController 클래스
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
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("정상적으로 삭제되었습니다.");
}
}
changeProductName() 메서드에서 쓰이는 ChangeProductNameDto 클래스
package com.springboot.jpa.data.dto;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ChangeProductNameDto {
private Long number;
private String name;
}
롬복의 주요 어노테이션
@Getter, @Setter
클래스에 선언돼 있는 필드에 대한 getter/setter 메서드를 생성한다. 코드의 라인 수를 줄이는 데 효과적이다.
생성자 자동 생성 어노테이션
- NoArgsConstructor: 매개변수가 없는 생성자를 자동 생성한다.
- AllArgsConstructor: 모든 필드를 매개변수로 갖는 생성자를 자동 생성한다.
- RequiredArgsConstructor: 필드 중 final이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동 생성한다.
@ToString
toString() 메서드를 생성하는 어노테이션이다.
toString() 메서드는 필드의 값을 문자열로 조합해서 리턴한다. 또한 민감한 정보처럼 숨겨야 할 정보가 있다면
@ToString(exclude = "name")
과 같이 exclude 속성을 사용해 특정 필드를 자동생성에서 제외할 수 있다.
@EqualsAndHashCode
@EqualsAndHashCode는 객체의 동등성(Equality)과 동일성(Identity)을 비교하는 연산 메서드를 생성한다.
- equals: 두 객체의 내용이 같은지 동등성을 비교한다.
- hashCode: 두 객체가 같은 객체인지 동일성을 비교한다.
만약 부모 클래스가 있어서 상속을 받는 상황이라면 부모 클래스의 필드까지 비교할 필요가 있는 경우도 발생하는데 이 경우에는 @EqualsAndHashCode에서 제공하는 callSuper 송성을 설정한다.
@EqualsAndHashCode(callSuper = true)
callSuper의 기본값은 false이며, true일 경우 부모 객체의 값도 비교 대상에 포함된다.
@Data
@Data는 @Getter/@Setter, @RequiredArgsConstructor, @ToString, @EqaulsAndHashCode를 모두 포괄하는 어노테이션이다.
'Springboot > 이론' 카테고리의 다른 글
| [Springboot] Spring Data JPA 활용 - 정렬과 페이징 (0) | 2023.11.10 |
|---|---|
| [Springboot] Spring Data JPA 활용 - JPQL (0) | 2023.11.10 |
| [Springboot] 데이터베이스 연동 - ORM, JPA (0) | 2023.11.05 |
| [Springboot] API를 작성하는 다양한 방법 (0) | 2023.10.29 |
| [Springboot] 스프링 부트 애플리케이션 개발하기 (0) | 2023.10.29 |