6. 데이터베이스 연동
6.1 마리아DB 설치
6.2 ORM
- ORM은 Object Relational Mapping의 줄임말로 객체 관계 매핑을 의미한다.
- 자바와 같은 객체지향 언어에서 의미하는 객체(클래스)와 RDB(Relational Database)의 테이블을 자동으로 매핑하는 방법이다.
- ORM을 이용하면 쿼리문 작성이 아닌 코드(메서드)로 데이터를 조작할 수 있다.
6.2.1 ORM 장점
- ORM을 사용하면서 데이터베이스 쿼리를 객체지향적으로 조작할 수 있다. (비용 절감 및 가독성 증가한다.)
- 재사용 및 유지보수가 편리하다. (ORM을 통해 매핑된 객체는 모두 독립적으로 작성되어 재사용 용이 및 유지보수 수월하다.)
- 데이터베이스에 대한 종속성이 줄어든다. (ORM을 통해 자동 생성된 SQL문은 객체를 기반으로 데이터베이스 테이블을 관리하기 때문에 데이터베이스에 종속적이지 않다.)
6.2.2 ORM 단점
- ORM만으로 온전한 서비스를 구현하기에는 한계가 있다. (복잡한 서비스의 경우 직접 쿼리를 구현하지 않고 코드로 구현하기 어렵고 , 정확한 설계 없이 ORM만으로 구현시 속도 저하 등의 성능 문제가 발생할 수 있다.)
- 애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치가 발생한다.
- 세분성: 데이터베이스에 있는 테이블의 수와 애플리케이션의 엔티티 클래스의 수가 다른 경우가 생긴다.
- 상속성: RDBMS에는 상속이라는 개념이 없다.
- 식별성: RDBMS는 기본 키로 동일성을 정의한다. 하지만 자바는 두 객체의 값이 같아도 다르다고 판단할 수 있다.
- 연관성: 객체지향 언어는 객체를 참조함으로써 연관성을 나타내지만 RDBMS에서는 외래 키를 삽입함으로써 연관성을 표현한다. 또한 객체지향 언어에서 객체를 참조할 때는 방향성이 존재하지만 RDBMS에서 외래 키를 삽입하는 것은 양방향 관계를 가지기 때문에 방향성이 없다.
- 탐색: 자바에서는 특정 값에 접근하기 위해 객체 참조 같은 연결 수단을 활용한다. 반면 RDBMS에서는 쿼리를 최소화하고 조인을 통해 여러 테이블을 로드하고 값을 추출하는 접근 방식을 채택하고 있다.
6.3 JPA
- JPA(Java Persistence API)는 ORM 기술 표준으로 채택된 인터페이스 모음이며, ORM의 구체화된 자바 표준 스펙이다.
- JPA의 메커니즘을 보면 내부적으로 JDBC를 사용한다.
- JPA는 적절한 SQL을 생성하고 데이터베이스를 조작해서 객체를 자동 매핑하는 역할을 수행한다.
- JPA 기반의 구현체는 대표적으로 3가지가 있다.
- 하이버네이트
- 이클립스 링크
- 데이터 뉴클리어스
6.4 하이버네이트
6.4.1 Spring Data JPA
- Spring Data JPA는 JPA를 편리하게 사용할 수 있도록 지원한다.
- CRUD 처리에 필요한 인터페이스를 제공하며, 하이버네이트의 엔티티 매니저(EntityManager)를 직접 다루지 않고 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작한다.
- 이를 통해 자주 사용되는 기능을 더 쉽게 사용할 수 있게 구현한 라이브러리이다.
6.5 영속성 컨텍스트
- 영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행한다.
- 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업을 수행한다. 이처럼 엔티티 객체가 영속성 컨텍스트에 들어와 JPA의 관리 대상이 되는 시점부터는 해당 객체를 영속 객체(Persistence Object)라고 부른다.
6.5.1 엔티티 매니저
- 엔티티 매니저(EntityManager)는 엔티티를 관리하는 객체이다.
- 엔티티 매니저는 데이터베이스에 접근해서 CRUD 작업을 수행한다.
6.5.2 엔티티 생명주기
- 비영속(New)
영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태 - 영속영(Managed)
속성 컨텍스트에 의해 엔티티 객체가 관리되는 상태 - 준영속(Detached)
영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태 - 삭제(Removed)
데이터베이스에서 레코드를 삭제하기 위해 영속성 컨텍스트에 삭제요청을 한 상태
6.6 데이터베이스 연동
6.6.1 프로젝트 생성
6.7 엔티티 설계
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;
___getter/setter 메서드 생략___
}
6.7.1 엔티티 관련 기본 어노테이션
관련 기본 어노테이션
- @Entity
해당 클래스가 엔티티임을 명시하기 위한 어노테이션이다.
테이블과 일대일로 매칭되며, 테이블에서 하나의 레코드를 의미한다.
@Entity
public class User {
// ...
}
- @Table
특별한 경우가 아니면 필요하지 않는다.
클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우 사용한다.
@Entity
@Table(name = "users")
public class User {
// ...
}
- @Id
테이블의 기본값 역할로 사용한다.
모든 엔티티는 @Id 어노테이션이 필요하다.
@Entity
public class User {
@Id
private Long id;
// ...
}
- @GeneratedValue
필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용한다.
1) 사용하지 않는 방식(직접 할당)
애플리케이션에서 자체적으로 고유한 기본값을 생성할 경우 사용하는 방식
내부에 정해진 규칙에 의해 기본값을 생성하고 식별자로 사용한다.
@Entity
public class User {
@Id
private String userId;
// ...
}
2) AUTO
@GeneratedValue의 기본 설정값
기본값을 사용하는 데이터베이스에 맞게 자동 생성한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// ...
}
3) IDENTITY
기본값 생성을 데이터베이스에 위임하는 방식이다.
AUTO_INCREMENT를 사용해 기본값을 생성한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
}
4) SEQUENCE
식별자 생성기를 설정하고 이를 통해 값을 자동 주입받는다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_seq", allocationSize = 1)
private Long id;
// ...
}
5) TABLE
어떤 DBMS를 사용하더라도 동일하게 동작하기를 원할 경우 사용한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "user_gen")
@TableGenerator(name = "user_gen", table = "id_gen", pkColumnName = "gen_name", valueColumnName = "gen_val", allocationSize = 1)
private Long id;
// ...
}
- @Column
자동으로 테이블 칼럼으로 매핑된다.
별다른 설정을 하지 않을 예정이라면 생략이 가능하다.
- name: 데이터베이스의 칼럼명을 설정하는 속성이다. 명시하지 않으면 필드명으로 지정된다.
- nullable: 칼럼 값에 null 처리가 가능한지를 명시하는 속성이다.
- length: 데이터의 최대 길이를 설정한다.
- unique: 해당 칼럼을 유니크로 설정한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "username", nullable = false, length = 50, unique = true)
private String username;
// ...
}
- @Transient
엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서는 필요 없을 경우 사용된다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Transient
private String temporaryPassword;
// ...
}
6.8 리포지토리 인터페이스 설계
- JpaRepository를 기반으로 더욱쉽게 DB를 사용할 수 있는 아키텍처를 제공한다.
- JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.
6.8.1 리포지토리 인터페이스 생성
- 리포지토리를 생성하기 위해서는 접근하려는 테이블과 매핑되는 엔티티에 대한 인터페이스를 생성하고, JpaRepository를 상속받으면 된다.
- 엔티티를 사용하기 위해서는 대상 엔티티를 Product로 설정하고 해당 엔티티의 @Id 필드 타입인 Long을 설정한다.
6.8.1 리포지토리 메서드의 생성 규칙
- 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: 두 값(숫자) 사이의 데이터를 조회한다.
6.9 DAO 설계
- DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다.
- 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행한다.
- 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있다.
- 규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 데이터베이스에 접근해서 구현하기도 한다.
6.9.1 DAO 클래스 생성
public class UserDAOImpl implements UserDAO {
@Override
public void insert(User user) {
// 데이터베이스 연결 및 삽입 쿼리 실행 코드 작성
}
@Override
public User getById(int id) {
// 데이터베이스 연결 및 조회 쿼리 실행 코드 작성
return new User(/* id를 기반으로 한 사용자 데이터 로드 */);
}
@Override
public User findById(int id) {
// 데이터베이스 연결 및 조회 쿼리 실행 코드 작성
return new User(/* id를 기반으로 한 사용자 데이터 로드 */);
}
@Override
public void update(User user) {
// 데이터베이스 연결 및 업데이트 쿼리 실행 코드 작성
}
@Override
public void delete(int id) {
// 데이터베이스 연결 및 삭제 쿼리 실행 코드 작성
}
@Override
public void save(User user) {
if (findById(user.getId()) != null) {
update(user);
} else {
insert(user);
}
}
}
6.10 DAO 연동을 위한 컨트롤러와 서비스 설계
- 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 한다.
- 먼저 DAO의 메서드를 호출하고 그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후 컨트롤러를 생성해야한다.
6.10.1 DAO 서비스 클래스 만들기
- 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;
}
- 데이터베이스와 밀접한 관련이 있는 데이터 액세스 레이어까지는 엔티티 객체를 사용하고, 클라이언트와 가까워지는 다른 레이어에서는 데이터를 교환하는 데 DTO 객체를 사용하는 것이 일반적이다.
- 구현체 클래스
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@Override
public ProductResponseDto getProduct(Long number) {
Product product = productRepository.findById(number)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
return new ProductResponseDto(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 = productRepository.save(product);
return new ProductResponseDto(product.getName(), product.getPrice(), product.getStock());
}
@Override
public ProductResponseDto changeProductName(Long number, String name) throws Exception {
Product product = productRepository.findById(number)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
product.setName(name);
product = productRepository.save(product);
return new ProductResponseDto(product.getName(), product.getPrice(), product.getStock());
}
@Override
public void deleteProduct(Long number) throws Exception {
Product product = productRepository.findById(number)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
productRepository.delete(product);
}
}
6.10.2 컨트롤러 생성
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
@CrossOrigin
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{number}")
public ProductResponseDto getProduct(@PathVariable Long number) {
return productService.getProduct(number);
}
@PostMapping
public ProductResponseDto saveProduct(@RequestBody ProductDto productDto) {
return productService.saveProduct(productDto);
}
@PutMapping("/{number}")
public ProductResponseDto changeProductName(@PathVariable Long number, @RequestParam String name) throws Exception {
return productService.changeProductName(number, name);
}
@DeleteMapping("/{number}")
public void deleteProduct(@PathVariable Long number) throws Exception {
productService.deleteProduct(number);
}
}
- GET /products/{number}: 주어진 ID에 해당하는 상품을 검색.
- POST /products: 새 상품을 저장.
- PUT /products/{number}: 주어진 ID에 해당하는 상품의 이름을 변경.
- DELETE /products/{number}: 주어진 ID에 해당하는 상품을 삭제.
6.10.2 Swagger API를 통한 동작 확인
https://yoonhs98.tistory.com/34
스프링 부트 핵심 가이드 5장 [API를 작성하는 다양한 방법]
5장 API를 작성하는 다양한 방법 5.1 프로젝트 설정 4장과 동일 5.2 GET API 만들기 GET API는 웹 애플리케이션 서버에서 값을 가져올 때 사용하는 API이다. 컨트롤러 클래스에 @RestController, @RequestMapping
yoonhs98.tistory.com
5.6 [ 한걸음 더 ] REST API 명세를 문서화하는 방법 – Swagger 참조
6.11 [한걸음 더] 반복되는 코드의 작성을 생략하는 방법 – 롬복
- 데이터 클래스를 생성할 때 반복적으로 사용하는 getter/setter 같은 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리이다.
6.11.1 롬복 설치
롬복(Lombok)을 프로젝트에 추가하려면 먼저 빌드 도구별로 의존성을 추가해야 한다.
- Maven
Maven을 사용하는 경우 `pom.xml` 파일에 다음 의존성을 추가해야 한다.
xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
- Gradle
Gradle을 사용하는 경우 `build.gradle` 파일에 다음 의존성을 추가한다.
groovy
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
}
위 의존성 추가 후, 롬복이 설치된다. 하지만 이 작업만으로는 IDE에서 롬복 어노테이션을 인식하거나 제대로 사용할 수 없다. IDEA, Eclipse 등 주요 IDE에 롬복 플러그인을 설치해야 한다.
- IntelliJ IDEA
1. `File` > `Settings`를 클릭 (Mac의 경우 `IntelliJ IDEA` > `Preferences`).
2. 왼쪽 패널에서 `Plugins`를 선택.
3. 검색 상자에 "Lombok"을 입력하고 검색된 결과에서 `Lombok` 플러그인을 찾아 설치.
4. 설치가 완료되면, IntelliJ IDEA를 다시 시작.
- Eclipse
1. Eclipse를 실행한 상태에서 `Help` > `Eclipse Marketplace`를 클릭.
2. 검색 상자에 "Lombok"을 입력하고 검색된 결과에서 `Lombok` 플러그인을 찾아 설치.
3. 설치가 완료되면, Eclipse를 다시 시작.
6.11.2 롬복 적용
- 적용 전
package com.springboot.jpa.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() {
}
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 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 Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}
- 적용 후
package com.springboot.jpa.data.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@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;
}
6.11.3 롬복의 주요 어노테이션
1. @Getter와 @Setter: 이 어노테이션은 클래스의 모든 필드에 대한 getter 및 setter 메서드를 자동으로 생성해준다. 개별 필드에도 적용할 수 있다.
2. @NoArgsConstructor: 인자 없는 기본 생성자를 자동으로 생성해준다. 필요에 따라 'AccessLevel'을 명시하여 접근 제한을 설정할 수 있다.
3. @AllArgsConstructor: 클래스의 모든 필드를 인자로 받는 생성자를 자동으로 생성해준다. 마찬가지로 'AccessLevel'을 통해 접근 제한을 설정할 수 있다.
4. @RequiredArgsConstructor: `final` 또는 `@NonNull`이 지정된 필드를 인자로 받는 생성자를 자동으로 생성해준다.
5. @Builder: 빌더 패턴을 적용하여 객체를 생성할 수 있게 해주는 빌더 클래스를 자동으로 생성해준다. 이를 통해 가독성이 높은 간결한 코드로 객체를 생성할 수 있다.
6. @Data: 여러 롬복 어노테이션을 한 번에 적용하여 클래스의 코드를 최소화한다. 이 어노테이션은 `@Getter`, `@Setter`, `@RequiredArgsConstructor`, `@ToString`, `@EqualsAndHashCode`를 포함한다.
7. @Value: 불변 객체(Immutable Object)를 생성할 때 사용된다. 이 어노테이션은 `@AllArgsConstructor`, `@Getter`, `@ToString`, `@EqualsAndHashCode` 등을 포함하며, 모든 필드를 `final`로 설정한다.
8. @ToString: 클래스의 `toString` 메서드를 자동으로 생성해준다. 인스턴스의 상태를 문자열로 반환하는 기능을 쉽게 구현할 수 있다.
9. @EqualsAndHashCode: 클래스의 `equals` 및 `hashCode` 메서드를 자동으로 생성해준다. 객체의 동등성 및 해시 코드를 생성하는 방법에 대한 고민을 줄일 수 있다.
'북터디 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
스프링 부트 핵심 가이드 9장 [연관관계 매핑] (0) | 2023.08.20 |
---|---|
스프링 부트 핵심 가이드 8장 [Spring Data JPA 활용] (0) | 2023.08.14 |
스프링 부트 핵심 가이드 5장 [API를 작성하는 다양한 방법] (0) | 2023.07.31 |
스프링 부트 핵심 가이드 4장 [스프링 부트 애플리케이션 개발하기] (0) | 2023.07.30 |
스프링 부트 핵심 가이드 3장 [개발 환경 구성] (0) | 2023.07.24 |