이 글은 스프링 부트 핵심 가이드(장정우 저)를 읽고 개인적으로 정리하기 위한 글입니다.
마리아 DB 설치
- (https://mariadb.org/download/) 로 접속한다.
2. 개발 환경에 맞춰 설정하고 설치 파일을 내려 받는다.
3. 설치 프로그램 다운로드가 완료 되면 설치를 진행 한다.
4. 계속 Next를 진행하다가 마리아 DB의 root 패스워드 및 기본 인코딩 설정이 나오면 패스워드 입력 후 진행
5. 다음 서버 이름과 포트 번호를 입력하은 창이 나오면 원하는 포트와 이름을 적고 Next 진행 기본 포트는 3306
ORM
- Object Relational Mapping, 객체 관계 매핑
- 자바와 같은 객체지향 언어에서 의미하는 객체(클래스)와 RDB(Relational DB)의 테이블을 자동으로 매핑하는 방법
- 클래스는 DB 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 RDB 테이블과 어쩔 수 없는 불일치가 존재
- ORM은 이 둘(클래스, 테이블)의 불일치와 제약사항을 해결하는 역할
- ORM을 이용하면 쿼리문 작성이 아닌 코드(메서드)로 데이터를 조작 가능
ORM의 장점
- ORM을 사용하면서 데이터베이스 쿼리를 객체지향적으로 조작 가능
- 쿼리문을 작성하는 양이 현저히 줄어 개발 비용이 줄어든다.
- 객체지향적으로 데이터베이스에 접근할 수 있어 코드의 가독성을 높인다.
- 재사용 및 유지보수가 편리
- ORM을 통해 매핑된 객체는 모두 독립적으로 작성되어 있어 재사용이 용이
- 객체들은 각 클래스로 나뉘어 있어 유지보수가 수월
- 데이터베이스에 대한 종속성이 줄어든다.
- ORM을 통해 자동 생성된 SQL문은 객체를 기반으로 데이터베이스 테이블을 관리하기 때문에 데이터베이스에 종속적이지 않다.
- 데이터베이스를 교체하는 상황에서도 비교적 적은 리스크를 부담
ORM의 단점
- ORM만으로 온전한 서비스를 구현하기에 한계가 있다.
- 복잡한 서비스의 경우 직접 쿼리를 구현하지 않고 코드로 구현하기 어렵다.
- 복잡한 쿼리를 정확한 설계 없이 ORM만으로 구성하게 되면 속도 저하 등의 성능 문제가 발생할 수 있다.
- 애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치가 발생
- 세분성(Granularity) : ORM의 자동 설계 방법에 따라 데이터베이스에 있는 테이블 수와 애플리케이션의 엔티티(Entity)클래스의 수가 다른 경우가 생긴다.(클래스가 테이블의 수보다 많아질 수 있다.)
- 상속성(Inheritance) : RDBMS에는 상속 개념이 없다.
- 식별성(Identity) : RDBMS는 기본키(primary key)로 동일성을 정의. 하지만 자바는 두 객체의 값이 같아도 다르다고 판단할 수 있다.
- 연관성(Associations) : 객체지향 언어는 객체를 참조함으로써 연관성을 나타내지만 RDBMS에서는 외래키를 삽입함으로써 연관성을 표현, 또한 객체지향 언어에서는 객체를 참조할 때 방향성이 존재하지만 RDBMS에는 외래키를 삽입하는 것은 양방향의 관계를 가지기 때문에 방향성이 없다.
- 탐색(Navigation) : 자바와 RDBMS는 어떤 값(객체)에 접근하는 방식이 다르다. 자바는 특정 값에 접근하기 위해 객체 참조 같은 연결 수단 활용, 반면 RDBMS에는 쿼리를 최소화하고 조인을 통해 여러 테이블을 로드하고 값을 추출하는 접근 방식을 택하고 있다.
JPA
- Java Persistence API, 자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음
- ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함
- JPA는 실제로 동작하는 것이 아니고 어떻게 동작해야 하는지 메커니즘을 정리한 표준 명세
- JPA의 메커니즘을 보면 내부적으로 JDBC를 사용
- 개발자가 직접 JDBC를 구현하면 SQL에 의존하게 되는 문제 등이 있어 개발 효율이 떨어짐
- JPA는 이 같은 문제점을 보완, 개발자 대신 적절한 SQL을 생성하고 DB를 조작해서 객체를 자동 매핑하는 역할 수행
- JPA 기반의 구현체로는 대표적으로 세 가지가 있다.
하이버네이트
- 자바의 ORM 프레임워크, JPA가 정의하는 인터페이스를 구현하고 있는 JPA 구현체 중 하나
Spring Data JPA
- JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나
- CRUD 처리에 필요한 인터페이스 제공
- 하이버네이트의 엔티티 매니저를 직접 다루지 않고 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작
- 하이버네이트에서 자주 사용되는 기능을 더 쉽게 구현한 라이브러리
영속성 컨텍스트(Persistence Context)
- 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행
- 엔티티 객체가 영속성 컨텍스트에 들어오면 JPA는 엔티티 객체의 매핑 정보를 데이터베이스에 반영하는 작업 수행
- 엔티티 객체가 영속성 컨텍스트에 들어와 JPA 관리 대상이 되는 시점부터는 해당 객체를 영속 객체라고 부른다.
- 영속성 컨텍스트는 세션 단위의 생명주기를 가진다.
데이터베이스에 접근하기 위한 세션이 생성되면 영속성 컨텍스트가 만들어지고, 세션이 종료되면 영속성 컨텍스트도 없어진다.
엔티티 매니저(EntityManager)
- 엔티티를 관리하는 객체
- 데이터베이스에 접근해서 CRUD 작업을 수행
- Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터 베이스에 접근하는데,
실제 내부 구현체인 SimpleJpaRepository가 엔티티 매니저를 사용 - 엔티티 매니저는 엔티티 매니저 팩토리(EntityManagerFactory)가 만든다.
- 엔티티 매니저 팩토리는 데이터베이스에 대응하는 객체로서 스프링 부트에서는 자동 설정 기능이 있기 때문에
application.properties에 작성한 최소한의 설정만으로 동작하지만 JPA의 구현체 중 하나의 하이버네이트에서는 persistence.xml이라는 설정 파일을 구성하고 사용해야 하는 객체이다.
엔티티의 생명주기
- 엔티티 객체는 영속성 컨텍스트에서 다음과 같은 4가지 상태로 구분
- 비영속(New)
- 영속성 컨텍스트에 추가되지 않은 엔티티 객체의 상태
- 영속(Managed)
- 영속성 컨텍스트에 의해 엔티티 객체가 관리되는 상태
- 준영속(Detached)
- 영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태
- 삭제(Removed)
- 데이터베이스에서 레코드를 사제하기 위해 영속성 컨텍스트에 삭제 요청을 한 상태
데이터베이스 연동
프로젝트 생성
- 버전 : 2.5.6
- groupId : com.springboot
- name : jpa
- artifactId : jpa
- Developer : Lombok, Spring Configuration Processor
- Web : Srping Web
- SQL : Spring Data JPA, MariaDB Driver
엔티티 설계
- Spring Data JPA를 사용 시 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없음
- 이 기능을 사용하게 가능 하는 것이 엔티티(Entity)
- JPA에서는 엔티티는 데이터베이스의 테이블에 대응하는 클래스
- 엔티티에는 데이터베이스에 쓰일 테이블과 칼럼을 정의
엔티티 관련 기본 어노테이션
- @Entity
- 해당 클래스가 엔티티임을 명시하기 위한 어노테이션
- 클래스 자체는 테이블과 일대일로 매칭, 해당 클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미
- @Table
- 엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 @Table 어노테이션이 필요하지 않다.
- @Table 어노테이션을 사용할 때는 클래스의 이름과 테이블의 이름을 다르게 지정해야 하는 경우
- @Table 어노테이션을 명시하지 않으면 테이블의 이름과 클래스의 이름이 동일하다는 의미,
서로 다른 이름을 쓰려면 @Table(name = "값") 형태로 데이터베이스의 테이블을 명시해야 함 - 대체로 자바의 명명법과 데이터베이스가 사용하는 명명법이 다르기 때문에 자주 사용
- @Id
- 엔티티 클래스의 필드는 테이블의 칼럼과 매핑
- @Id 어노테이션이 선언된 필드는 테이블의 기본값 역할로 사용
- 모든 엔티티는 @Id 어노테이션이 필요
- @GeneratedValue
- 일반적으로 @Id 어노테이션과 함께 사용 시 해당 필드의 값을 어떤 방식으로 자동 생성할지 결정할 떄 사용
- GeneratedValue를 사용하지 않는 방식(직접 할당)
- 애플리케이션에서 자체적으로 고유한 기본값을 생성할 경우 사용하는 방식
- 내부에 정해진 규칙에 의해 기본값을 생성하고 식별자로 사용
- AUTO
- @GeneratedValue의 기본 설정값
- 기본값을 사용하는 데이터베이스에 맞게 자동 생성
- IDENTITY
- 기본값 생성을 데이터베이스에 위임하는 방식
- 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성
- SEQUENCE
- @SeqneceGenerator를 정의할 때는 name, sequeceName, allocationSize를 활용
- @GeneratedValue에 생성기를 설정
- TABLE
- 어떤 DBMS를 사용하더라도 동일하게 동작하기를 원할 경우 사용
- 식별자로 사용할 숫자의 보관 테이블을 별도로 생성해서 엔티티를 생성할 때마다 값을 갱신하며 사용
- @TableGenerator 어노테이션으로 테이블 정보를 설정
- @Column
- 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑, 그래서 별다른 설정을 하지 않을 예정이라면 이 어노테이션을 명시하지 않아도 괜찮다.
- name : 데이터베이스의 칼럼명을 설정하는 속성, 명시하지 않으면 필드명으로 지정
- nullable : 레코드를 생성할 떄 칼럼 값에 null 처리가 가능한지를 명시하는 속성
- length : 데이터베이스에 저장하는 데이터의 최대 길이를 설정
- unique : 해당 칼럼을 유니크로 설정
- @Transient
- 엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에서는 필요 없을 경우 이 어노테이션을 사용해 데이터베이스에서 이용하지 않게 할 수 있다.
리포지토리 인터페이스 설계
- Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공
- 스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면 기존의 다양한 메서드를 손쉽게 활용할 수 있다.
리포지토리 인터페이스 생성
import com.springboot.jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
JpaRepository에서 제공하는 기본 메서드
- findAll() 메소드
- Member 테이블에서 레코드 전체 목록을 조회
- List<Member> 객체가 리턴
- findById(id)
- Member 테이블에서 기본키 필드 값이 id인 레코드를 조회
- Optional<Member> 타입의 객체가 리턴
- 이 객체의 get 메서드를 호출하면 Member 객체가 리턴 예) Member m = memberRepository.findById(id).get();
- save(member)
- Member 객체를 Member 테이블에 저장
- 객체의 id(기본키) 속성값이 0이면 INSERT / 0이 아니면 UPDATE
saveAll(memberList) - Member 객체 목록을 Member 테이블에 저장
- delete(member)
- Member 객체의 id(기본키) 속성값과 일치하는 레코드를 삭제
- deleteAll(memberList)
- Member 객체 목록을 테이블에서 삭제
- count()
- Member 테이블의 전체 레코드 수를 리턴
- exists(id)
- Member 테이블에서 id에 해당하는 레코드가 있는지 true/false를 리턴
- flush()
- 지금까지 Member 테이블에 대한 데이터 변경 작업들이 디스크에 모두 기록
리포지토리 메서드의 생성 규칙
- FindBy
- SQL문의 where 절 역할을 수행하는 구분, findBy 뒤에 엔티티의 필드값을 입력해서 사용
- 예) findByName(String name)
- AND, OR
- 조건을 여러 개 설정하기 위해 사용
- 예) findByNameAnadEmail(String name, String email)
- Like/NotLike
- SQL문의 like와 동일한 기능 수행, 특정 문자를 포함하는지 여부를 조건으로 추가
- 비슷한 키워드로 Containing, Contains, isContaining이 있다.
- StartsWith/StartingWith
- 특정 키워드로 시작하는 문자열 조건을 설정
- EndsWith/EndingWith
- 특정 키워드로 끝나는 문자열 조건을 설정
- IsNull/IsNotNull
- 레코드 값이 Null이거나 Null이 아닌 값을 검색
- True/False
- 타입의 레코드를 검색할때 사용
- Before/After
- 시간을 기준으로 값을 검색
- LessThan/GreaterThan
- 특정 값(숫자)을 기준으로 대소 비교를 할 떄 사용
- Between
- 두 값(숫자) 사이의 데이터를 조회
- OrderBy
- SQL 문에서 order by와 동일한 기능을 수행
- 예) List<Member> findByNameOrderByAgeAsc(int age);
- countBy
- SQL 문의 count와 동일한 기능을 수행하며, 결괏값의 개수(count)를 추출
DAO 설계
- DAO(Data Access Object)는 데이트베이스에 접근하기 위한 로직을 관리하기 위한 객체
- 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행
- 다만 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있다.
DAO 클래스 생성
- DAO클래스는 일반적으로 '인터페이스-구현체' 구성으로 생성
- 일반적으로 데이터베이스에 접근하는 메서드는 리턴 값으로 데이터 객체를 전달
- 이때 객체를 엔티티 객체로 전달할지, DTO 객체로 전달할지에 대해서는 개발자마다 의견이 분분
- 일반적인 설계 원칙에서 엔티티 객체는 데이터베이스에 접근하는 계층에서만 사용하도록 정의
- 다른 계층으로 데이터를 전달할 때는 DTO 객체 사용
// ProductDAO 인터페이스
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;
}
// ProductDAO 인터페이스의 구현체 클래스
@Component
public class ProductDAOImpl implements ProductDAO {
private final 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.findById(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 클래스를 스프링이 관리하는 빈으로 등록하려면 @Componet or @Service 어노테이션 지정
- 빈으로 등록된 객체는 다른 클래스가 인터페이스를 가지고 의존성을 주입받을 때 이 구현체를 찾아 주입하게 된다.
DAO 연동을 위한 컨트롤러와 서비스 설계
- 앞에서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 한다.
- 먼저 DAO의 메서드를 호출하고 그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후 컨트롤러를 생성
서비스 클래스 만들기
- 서비스 레이어에서는 도메인 모델(Domain Model)을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공
- 핵심 기능을 구현하려면 세부 기능을 정의해야 한다. 세부 기능이 모여 핵심 기능을 구현
- 이런 모든 로직을 서비스 레이어에서 포함하기란 쉽지 않은 일이다 아키텍처의 한계를 극복하기 위해 아키텍처를 서비스 로직과 비즈니스 로직으로 분리하기도 한다.
- 도메인을 활용한 세부 기능들을 비즈니스 레이어의 로직에서 구현, 서비스 레이어에서는 기능들을 종합해서 핵심 기능을 전달하도록 구성하는 경우가 대표적
// ProductDto 클래스
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDto {
private String name;
private int price;
private int stock;
}
// ProductResponseDto 클래스
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
}
// ProductService 인터페이스
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에서 구현한 기능을 서비스 인터페이스에서 호출해 결과값을 가져오는 작업을 수행하도록 설계
- 서비스에서는 클라이언트가 요청한 데이터를 적절하게 가공해서 컨트롤러에게 넘기는 역할을 한다.
// ProductServiceImpl 클래스
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);
}
}
컨트롤러 생성
- 서비스 객체의 설계를 마친 후에는 비즈니스 로직과 클라이언트의 요청을 연결하는 컨트롤러를 생성해야 한다.
// ProductController
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("정상적으로 삭제되었습니다.");
}
}
'북스터디 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
Chapter 09. 연관관계 매핑 (0) | 2023.06.21 |
---|---|
Chapter 08. Spring Data JPA 활용 (0) | 2023.06.11 |
Chapter 05. API를 작성하는 다양한 방법 (0) | 2023.05.27 |
Chapter 04. 스프링 부트 애플리케이션 개발하기 (0) | 2023.05.27 |
Chapter 03. 개발 환경 구성 (0) | 2023.05.21 |