본 게시글은 '스프링 부트 핵심 가이드' 책의 내용을 정리한 것입니다.
저자 : 장정우
출판사 : 위키북스
6.1 MariaDB 설치
이 책은 MariaDB를 사용하여 실습한다.
설치 부분은 필요한 부분만 언급하고 넘어가려 한다.
※ 책에서 설치한 MariaDB 설치 환경
- MariaDB server Version : MariaDB Server 10.6.5
- Operation System : Windows
- Architecture : x86_64
- Package Type : MSI Package
※ 실무에서는 보안상 root 패스워드를 사용하지 않지만, 실습용이기에 root 계정을 사용함
※ [Use UTF8 as default server's character set] 체크!! 문자 인코딩 방식을 UTF-8을 기본값으로 설정
(UTF-8로 변경하는 법 : https://reminisce057.tistory.com/20)
※ MariaDB 기본 포트는 3306.
※ 책에서는 HeidiSQL을 사용하지만 Dbeaver 사용 예정
6.2 ORM
Object Relational Mapping(객체 관계 매핑)의 줄임말로 객체지향 언어에서 의미하는 객체(클래스)와 RDB(Relational Database)의 테이블을 자동으로 매핑하는 방법이다. 클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 RDB 테이블과 불일치가 존재한다. ORM은 이 둘의 불일치와 제약사항을 해결하는 역할이다.
ORM을 이용하면 쿼리문 작성이 아닌 코드(메서드)로 데이터 조작이 가능하다.
ORM의 장점
① ORM을 사용하면서 데이터베이스 쿼리를 객체지향적으로 조작할 수 있다.
- 쿼리문을 작성하는 양이 줄어 개발비용이 줄어든다.
- 객체지향적으로 데이터베이스에 접근할 수 있어 코드의 가독성을 높인다.
② 재사용 및 유지보수가 편리하다.
- ORM을 통해 매핑된 객체는 모두 독립적으로 작성되어 있어 재사용이 용이하다.
- 객체들은 각 클래스로 나뉘어 있어 유지보수가 수월하다.
③ 데이터베이스에 대한 종속성이 줄어든다.
- ORM을 통해 자동 생성된 SQL문은 객체를 기반으로 데이터베이스 테이블을 관리하기 때문에 데이터베이스에 종속적이지 않다.
- 데이터베이스를 교체하는 상황에서도 비교적 적은 리스크를 부담한다.
ORM의 단점
① ORM만으로 온전한 서비스를 구현하기에는 한계가 있다.
- 복잡한 서비스의 경우 직접 쿼리를 구현하지 않고 코드로 구현하기 어렵다.
- 복잡한 쿼리를 정확한 설계 없이 ORM만으로 구성하게 되면 속도 저하 등의 성능 문제가 발생할 수 있다.
② 애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치가 발생한다.
- 세분성(Granularity) : ORM의 자동 설계 방법에 따라 데이터베이스에 있는 테이블의 수와 애플리케이션의 엔티티(Entity) 클래스의 수가 다른 경우가 생긴다. (클래스가 테이블의 수보다 많아질 수 있다.)
- 상속성(Inheritance) : RDBMS에는 상속이라는 개념이 없다.
- 식별성(Identity) : RDBMS는 기본키(primary key)로 동일성을 정의한다. 하지만 자바는 두 객체의 값이 같아도 다르다고 판단할 수 있다.(식별과 동일성의 문제)
- 연관성(Associations) : 객체지향 언어는 객체를 참조함으로써 연관성을 나타내지만 RDBMS에서는 외래키(foreign key)를 삽입함으로써 연관성을 표현한다. 또한 객체지향 언어에서 객체를 참조할 때는 방향성이 존재하지만 RDBMS에서 외래키를 삽입하는 것은 양방향의 관계를 가지기 때문에 방향성이 없다.
- 탐색(Navigation) : 자바와 RDBMS는 어떤 값(객체)에 접근하는 방식이 다르다. 자바에서는 특정 값에 접근하기 위해 객체 참조 같은 연결 수단을 활용한다. 이 방식은 객체를 연결하고 또 연결해서 접근하는 그래프 형태의 접근 방식이다. 반면 RDBMS에서는 쿼리를 최소화하고 조인(JOIN)을 통해 여러 테이블을 로드하고 값을 추출하는 접근 방식을 채택한다.
6.3 JPA
JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다. ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함한다. 즉, JPA 또한 실제로 동작하는 것이 아닌 어떻게 동작해야 하는지 메커니즘을 정리한 표준 명세라고 할 수 있다.
JPA는 내부적으로 JDBC를 사용한다. 개발자가 직접 JDBC를 구현하면 SQL에 의존하게 되는 문제 등이 있어 개발의 효율성이 떨어지나, JPA는 이러한 문제점을 보완하여 개발자 대신 적절한 SQL을 생성하고 데이터베이스를 조작해서 객체를 자동 매핑하는 역할을 수행한다.
JPA 기반 구현체는 대표적으로 하이버네이트(Hibernate), 이클립스 링크(EclipseLink), 데이터 뉴클리어스(DataNucleus)가 있으며 하이버네이트가 가장 많이 사용된다.
6.4 하이버네이트
하이버네이트는 자바의 ORM프레임워크로, JPA가 정의하는 인터페이스를 구현하고 있는 JPA 구현체 중 하나이다.
6.4.1 Spring Data JPA
Spring Data JPA는 JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나이다. CRUD 처리에 필요한 인터페이스를 제공하며, 하이버네이트의 엔티티 매니저(EntityManager)를 직접 다루지 않고 리포지토리(Repository)를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작하여 하이버네이터에서 자주 사용되는 기능을 쉽게 사용할 수 있게 구현한 라이브러리이다.
6.5 영속성 컨텍스트
영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행한다. 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행한다. 엔티티 객체가 영속성 컨텍스트에 들어와 JPA의 관리 대상이 되는 시점부터 해당 객체를 영속 객체(Persistence Object)라고 부른다.
영속성 컨텍스트는 데이터베이스 접근을 위한 세션이 생성되면 만들어지고, 종료되면 같이 없어지는 세션 단위의 생명주기를 가진다. 엔티티매니저는 이러한 과정에서 영속성 컨텍스트에 접근하기 위한 수단으로 사용된다.
6.5.1 엔티티 매니저
엔티티 매니저(EntityManager)는 엔티티를 관리하는 객체이다. 엔티티 매니저는 데이터베이스에 접근해서 CRUD 작업을 수행한다. Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터베이스에 접근하는데 이때 리포지토리에서 엔티티 매니저를 사용한다.
엔티티 매니저는 엔티티 매니저 팩토리(EntityManagerFactory)가 만든다. 엔티티 매니저 팩토리는 데이터베이스에 대응하는 객체로서 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정만으로도 동작하지만 JPA의 구현체 중 하나인 하이버네이트에서는 persistence.xml이라는 설정 파일을 구성하고 사용해야 한다.
엔티티 매니저 팩토리는 애플리케이션에서 단 하나만 생성되며, 모든 엔티티가 공유해서 사용한다. 엔티티 매니저 팩토리로 생성된 엔티티 매니저는 엔티티를 영속성 컨텍스트에 추가해서 영속 객체로 만드는 작업을 수행하고, 영속성 컨텍스트와 데이터베이스를 비교하면서 실제 데이터베이스를 대상으로 작업을 수행한다.
6.5.2 엔티티의 생명주기
엔티티 객체는 영속성 컨텍스트에서 4가지 상태로 구분된다.
- 비영속(New) : 영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태를 의미
- 영속(Managed) : 영속성 컨텍스트에 의해 엔티티 객체가 관리되는 상태
- 준영속(Detached) : 영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태
- 삭제(Removedd) : 데이터베이스에서 레코드를 삭제하기 위해 영속성 컨텍스트에 삭제 요청을 한 상태
6.6 데이터베이스 연동
6.6.1 프로젝트 생성
groupId : com.springboot
name : jpa
artifactId : jpa
추가 의존성
Lombok, Spring Configuration Processor, Spring Web, Spring Data JPA, MariaDB Driver
pom.xml 파일에 Swagger 의존성 추가
(앞 챕터에서 사용한 SwaggerConfiguration.java(내용수정필요)와 logback-spring.xml 파일 가져오기)
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.springboot.jpa")) //basePackage 수정
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot Open API Test with Swagger")
.description("설명 부분")
.version("1.0.0")
.build();
}
}
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#
//MariaDB 계정 정보 입력
//하이버네이트 사용할 때 활성화할 수 있는 사항
spring.jpa.hibernate.ddl-auto=update //데이터베이스를 자동으로 조작
spring.jpa.show-sql=true //하이버네이트가 생성한 쿼리문을 출력하는 옵션
spring.jpa.properties.hibernate.format_sql=true //쿼리문을 보기 좋게 출력하기 위한 옵션
ddl-auto의 옵션사항
- create : 애플리케이션이 가동되고 SessionFactory가 실행될 때 기존 테이블을 지우고 새로 생성
- create-drop : create와 동일한 기능을 수행하나 애플리케이션 종료하는 시점에 테이블 지운다.
- update : SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마를 갱신한다. 기존에 저장된 데이터는 유지된다.
- validate : upate처럼 객체를 검사하지만 스키마는 건드리지 않는다. 검사 과정에서 데이터베이스의 테이블 정보와 객체의 정보가 다르면 에러 발생한다.
- none : ddl-auto 기능 사용하지 않는다.
운영환경에서는 대체로 validate와 none을 사용하고 그 외 기능은 사용하지 않는다. 하지만 개발환경에서는 create와 update를 사용하는 편이다.
6.7 엔티티 설계
쿼리문 없이 테이블 생성을 가능하게 해주는 것이 엔티티이다. JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스로, 엔티티에서 데이터베이스에 쓰일 테이블과 칼럼을 정의한다. 엔티티에 어노테이션을 사용하면 테이블 간의 연관관계를 정의할 수 있다.
다음처럼 엔티티 클래스를 생성한다.
package com.springboot.jpa.data.entity;
import java.time.LocalDateTime;
import lombok.*;
import javax.persistence.*;
@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;
}
6.7.1 엔티티 관련 기본 어노테이션
@Entity
해당 클래스가 엔티티임을 명시하기 위한 어노테이션이다. 클래스 자체는 테이블과 일대일로 매칭되며, 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미한다.
@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
엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑된다. @Column 어노테이션은 필드에 설정을 더할 때 사용한다.
◎ 자주 사용하는 요소
- name : 데이터베이스의 칼럼명을 설정하는 속성. 명시하지 않으면 필드명으로 지정
- nullable : 레코드를 생성할 때 칼럼 값에 null 처리가 가능한지를 명시하는 속성
- length : 데이터베이스에 저장하는 데이터의 최대 길이를 설정
- unique : 해당 칼럼을 유니크로 설정
@Transient
엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서 필요 없을 경우 사용한다.
6.8 리포지토리 인터페이스 설계
6.8.1 리포지토리 인터페이스 생성
리포지토리는 Spring Data JPA가 제공하는 인터페이스이다. 엔티티가 생성한 데이터베이스에 접근하는 데 사용되며 다음과 같이 JpaRepository를 상속받아 생성한다.
public interface ProductRepository extends JpaRepository<Product, Long> {
}
<> 안에 접근하려는 엔티티와 @Id 필드 타입을 설정한다.
6.8.2 리포지토리 메서드의 생성 규칙
리포지토리에서는 몇 가지 명명규칙에 따라 커스텀 메서드도 생성할 수 있다. 생성해야 하는 메서드는 대부분 Read 부분에 해당하는 Select 쿼리이다. 리포지토리에서 기본적으로 제공하는 조회 메서드가 기본값으로 단일 조회하거나 전체 엔티티를 조회하는 것만 지원하고 있기 때문이다.
메서드에 이름을 붙일 때는 첫 단어를 제외한 이후 단어들의 첫 글자를 대문자로 설정해야 JPA에서 정상적으로 인식한다. 조회 메서드(find)에 조건으로 붙일 수 있는 기능은 다음과 같다.
- findBy : SQL 문의 where 절 역할을 수행. findBy 뒤에 엔티티의 필드값을 입력해서 사용.
- AND, OR : 조건을 여러 개 설정하기 위해 사용
- Like / NotLike : SQL문의 like와 동일한 기능을 수행하며, 특정 문자를 포함하는지 여부를 조건으로 추가한다. 비슷한 키워드로 Containing, Contains, isContaining이 있다.
- StartsWith / StrartingWith : 특정 키워드로 시작하는 문자열 조건을 설정
- EndsWith / EndingWith : 특정 키워드로 끝나는 문자열 조건을 설정
- IsNull / IsNotNull : 레코드 값이 NUll이거나 Null이 아닌 값을 검색
- True / False : Boolean 타입의 레코드를 검색할 때 사용
- Before / After : 시간을 기준으로 값을 검색
- LessThan / GreaterThan : 특정 값(숫자)을 기준으로 대소 비교를 할 때 사용
- Between : 두 값(숫자) 사이의 데이터를 조회
- OrderBy : SQL문에서 order by와 동일한 기능을 수행
- CountBy : SQL문의 count와 동일한 기능을 수행하며, 결괏값의 개수를 추출
6.9 DAO 설계
DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다. 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행한다. 다만 Spring Data JPA에서 DAO의 개념은 리포지토리가 대체한다.
규모가 작을 경우 DAO를 별도로 구성하지 않고 서비스 레이어에서 DB에 접근하기도 하지만 실제로 업무에서는 데이터를 다루는 중간 계층을 두는 것이 유지보수 측면에서 용이하다.
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;
}
일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달한다. 보통 데이터베이스에 접근하는 계층에서는 데이터 객체를 엔티티 객체로 전달하고, 다른 계층으로 전달할 때는 DTO 객체를 사용하지만 회사나 부서마다 다르다.
DAO 인터페이스의 구현체 클래스
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 java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ProductDAOImpl implements ProductDAO {
//@Autowired 끝까지 리포지토리 의존성 주입하는 코드
private 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();
}
}
}
ProductDAOImpl 클래스를 스프링이 관리하는 빈으로 등록하기 위해 @Service나 @Component 어노테이션을 사용한다. 빈으로 등록된 객체는 다른 클래스가 인터페이스를 가지고 의존성을 주입받을 때 이 구현체를 찾아 주입하게 된다.
DAO 객체에서도 데이터베이스에 접근하기 위해 리포지토리 인터페이스를 사용해 의존성을 주입받는다.
insertProduct() 메서드는 Product 엔티티를 데이터베이스에 저장하는 기능을 수행한다. 리포지토리에 따로 메서드 구현할 필요 없이 JPA에서 기본적으로 제공하는 save 메서드를 사용할 수 있다.
selectProduct() 메서드는 조회를 위한 메서드이다. 리포지토리에서는 조회를 위해 getById()와 findById() 메서드를 제공하는데, 세부적인 부분에서 차이가 있다.
- getById() : 내부적으로 엔티티매니저의 getReference() 메서드를 호출하고, 호출하면 프락시 객체를 리턴한다. 실제 쿼리는 프락시 객체를 통해 최초로 데이터에 접근하는 시점에 실행된다. 데이터가 존재하지 않는 경우 EntityNotFoundException이 발생한다.
- findById() : 내부적으로 엔티티매니저의 find() 메서드를 호출한다. 이 메서드는 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않는다면 실제 데이터베이스에서 데이터를 조회한다. 리턴값으로 Optional 객체를 전달한다.
updateProductName() 메서드는 상품명을 업데이트하는 메서드이다. JPA에서 데이터 값을 변경할 때 영속성 컨텍스트를 활용해 값을 갱신하는데, find() 메서드를 통해 데이터베이스에서 값을 가져오면 가져온 객체가 영속성 컨텍스트에 추가된다. 영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행하면 JPA에서는 더티 체크(Dirty Check)라고 하는 변경 감지를 수행한다. 변경이 감지되면 대상 객체에 해당하는 데이터베이스의 레코드를 업데이트하는 쿼리가 실행된다.
deleteProduct() 메서드는 삭제를 위한 메서드이다. 데이터베이스의 레코드를 삭제하기 위해서는 삭제하고자 하는 레코드와 매핑된 영속 객체를 영속성 컨텍스트에 가져와야 한다. 따라서 findById() 메서드를 통해 객체를 가져오고, delete() 메서드를 통해 해당 객체를 삭제한다.
6.10 DAO 연동을 위한 컨트롤러와 서비스 설계
6.10.1 서비스 클래스 만들기
서비스 레이어는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공한다. 핵심 기능을 구현하려면 핵심 기능을 구성하는 세부 기능을 정의해야 한다. 이러한 모든 로직을 서비스 레이어에 포함하는 것은 쉽지 않기에 도메인을 활용한 세부 기능들을 비즈니스 레이어의 로직에서 구현하고, 서비스 레이어에서는 기능들을 종합해서 핵심 기능을 전달하도록 구성하기도 한다.
(실습 프로젝트에서는 서비스 레이어에서 비즈니스 로직을 처리한다.)
서비스객체는 DAO와 마찬가지로 추상화해서 구성한다.
DTO 클래스도 생성한다. ProductDto와 ProductResponseDto 클래스를 작성한다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
}
※ 빌더 메서드
빌더 패턴을 따르는 메서드이다. 데이터 클래스를 사용할 때 생성자로 초기화할 경우 모든 필드에 값을 넣거나 null을 명시적으로 사용해야 한다. 빌더 패턴을 사용하면 필요한 데이터만 설정할 수 있어 유연성을 확보할 수 있다.
서비스 인터페이스를 다음과 같이 작성한다.
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 객체를 사용하고, 데이터베이스와 밀접한 관련이 있는 데이터 액세스 레이어까지는 엔티티 객체를 사용하는 것이 일반적이다. 그리고 DTO 객체와 Entity 객체를 서비스 레이어에서 일반적으로 변환해서 전달하는 역할도 수행한다.
단 이것은 실무 환경에 따라 다르다.
(엔티티 대신 DTO로 DAO 사이의 데이터 전달을 하기도 하고 단일 데이터나 소량의 데이터를 전달하는 경우 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 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);
}
}
서비스 인터페이스의 구현체이다.
DAO 인터페이스 선언 후 @Autowired를 지정한 생성자를 통해 의존성을 주입받는다.
각 메서드 내부에서 DAO에서 넘어온 엔티티 객체를 DTO 객체로 변환 후 반환하는 작업을 한다.
saveProduct() 메서드의 경우 넘어온 DTO 객체를 엔티티 객체로 초기화하여 DAO로 넘긴 뒤 반환받은 것을 다시 DTO 객체로 변환하여 반환한다.
일반적으로 저장하는 메서드는 리턴 타입을 void 타입으로 작성하거나 boolean 타입으로 작업의 성공 여부를 반환하는 경우가 많다. 하지만 이 실습 코드에서는 클라이언트가 저장한 데이터의 인덱스 값을 알기 위해 저장하면서 가져온 인덱스를 DTO에 담아 결괏값으로 클라이언트에 전달하는 방식으로 작성되었다. void 타입으로 메서드를 작성한다면 조회 메서드를 추가로 구현하고 클라이언트에서 한 번 더 요청해야 한다.
chageProductName() 메서드는 인덱스와 변경하려는 이름을 받아와 DAO로 넘기는데 기존 이름까지 받아와서 상품정보와 일치하는지 검증하면 더욱 견고한 코드를 작성할 수 있다.
deleteProduct() 메서드는 리포지토리에서 제공하는 delete() 메서드가 리턴 타입이 지정돼 있지 않기 때문에 void로 리턴 타입을 지정하여 구현했다.
6.10.2 컨트롤러 생성
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("정상적으로 삭제되었습니다.");
}
}
컨트롤러는 클라이언트로부터 요청을 받고 서비스 레이어에 구현된 메서드를 호출해서 결괏값을 받아 전달하는 역할만 맡는 것이 좋다.
기능에 대한 요청은 '컨트롤러 - 서비스 - DAO - 리포지토리' 순으로 이동하고, 응답은 그것의 역순으로 전달된다.
6.10.3 Swagger API를 통한 동작 확인
Swagger 페이지(http://localhost:8080/swagger-ui.html)로 접속 후 기능을 요청해 보고 결과를 확인한다.
Response Body에 있는 문구는 정상적으로 삭제가 되면 Body값에 해당 문구를 담아 전달하도록 컨트롤러를 구현했기 때문이다.
6.11 반복되는 코드의 작성을 생략하는 방법 - 롬복
롬복은 데이터(모델) 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리이다.
롬복의 장점
- 어노테이션 기반으로 코드를 자동 생성하므로 생산성이 높아진다.
- 반복되는 코드를 생략할 수 있어 가독성이 좋아진다.
- 롬복을 안다면 간단하게 코드를 유추할 수 있어 유지보수에 용이하다.
롬복의 단점
- 어노테이션이 자동 생성이기 때문에 메서드를 개발자의 의도대로 정확하게 구현하지 못하는 경우가 있다.
6.11.1 롬복 설치
pom.xml에 롬복 의존성 추가 코드 확인
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
[File] -> [Settings] -> [Build, Execution, Deployment] -> [Compiler] ->[Annotation Processors]를 선택 후 [Enable annotation processing] 항목에 체크 후 [OK] 버튼 누른다.
6.11.2 롬복 적용
엔티티 클래스와 DTO 클래스에서 @Getter, @Setter 등 롬복의 어노테이션들을 사용했었다.
롬복의 어노테이션이 실제로 어떤 메서드를 생성하는지는 롬복을 사용한 클래스에 마우스 오른쪽 버튼을 클릭한 후 [Refactor] -> [Delombok] -> [ All Lombok annotation]을 클릭하면 롬복의 어노테이션이 실제 코드로 리팩토링된다.(코드 확인 후에는 Ctrl + Z 를 눌러 원래 상태로 되돌린다.)
Delombok 사용하지 않고 확인하려면 좌측 하단에 [Structure]를 클릭하면 해당 클래스에 정의된 메서드 목록을 확인할 수 있다.
6.11.3 롬복의 주요 어노테이션
@Getter, @Setter
클래스에 선언돼 있는 필드에 대한 getter / setter 메서드를 생성한다.
생성자 자동 생성 어노테이션
데이터 클래스의 초기화를 위한 생성자를 자동으로 만들어주는 어노테이션
- @NoArgsConstructor : 매개변수가 없는 생성자를 자동 생성한다.
- @AllArgsConstructor : 모든 필드를 매개변수로 갖는 생성자를 자동 생성한다.
- @RequiredArgsConstructor : 필드 중 final이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동 생성한다.
@ToString
toString() 메서드를 생성하는 어노테이션. toString() 메서드는 필드의 값을 문자열로 조합해서 리턴한다. 민감한 정보처럼 숨겨야 할 정보가 있다면 @ToString어노테이션이 제공하는 exclude 속성을 사용해 특정 필요를 자동 생성에서 제외할 수 있다.
@ToString(exclude ="name") //제외할 필드를 입력한다.
@EqualsAndHashCode
@EqualsAndHashCode는 객체의 동등성(Equality)와 동일성(Identity)을 비교하는 연산 메서드를 생성한다.
이 어노테이션은 2개의 메서드를 생성해주는 것으로 equals 메서드는 두 객체의 내용이 같은지 동등성을 비교하고 hashCode 메서드는 두 객체가 같은 객체인지 동일성을 비교한다.
부모 클래스가 있어 상속을 받는 상황에서 부모 클래스의 필드까지 비교할 필요가 있을 경우 @EqualsAndHashCode에서 제공하는 callSuper 속성을 설정하여 부모 클래스의 필드를 비교 대상에 포함할 수 있다.
(기본값은 false, true일 때 부모 클래스의 필드도 비교 대상에 포함된다.)
@EqualsAndHashCode(callSuper = ture)
※ 동등성과 동일성
동등성은 비교 대상이 되는 두 객체가 가진 값이 같음을 의미하고, 동일성은 두 객체가 같은 객체임을 의미한다.
@Data
@Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode를 모두 포괄하는 어노테이션
'개발 지식 기록 > 북스터디' 카테고리의 다른 글
[스프링 부트 핵심 가이드] 09. 연관관계 매핑 (0) | 2023.09.24 |
---|---|
[스프링 부트 핵심 가이드] 08. Spring Data JPA 활용 (0) | 2023.09.17 |
[스프링 부트 핵심 가이드] 05. API를 작성하는 다양한 방법 (0) | 2023.09.03 |
[스프링 부트 핵심 가이드] 04. 스프링 부트 애플리케이션 개발하기 (0) | 2023.09.01 |
[스프링 부트 핵심 가이드] 03. 개발 환경 구성 (0) | 2023.08.27 |