저번 주차에 이어 데이터베이스 사용에 대해 알아보겠습니다.
5장과 동일하게 프로젝트를 생성합니다.
ArtifactId와 name은 'jpa'로 설정하고, 라이브러리는 사진과 같이 Lombok, Spring Web, Spring Data JPA, MariaDB Driver, Spring Configuration Processor를 선택하여 받아줍니다.
또한 5.6절에서 진행했던 Swagger 설정을 해줍니다.
// jpa/pom.xml
<dependencies>
...
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
...
</dependencies>
// config/SwaggerCofiguration.java
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// 스캔 범위 설정(com.springboot.api 하위 클래스 모두 스캔하여 문서 생성)
.apis(RequestHandlerSelectors.basePackage("com.springboot.jpa"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot Open API Test with Swagger")
.description("설명 부분")
.version("1.0.0")
.build();
}
}
Spring Data JPA 의존성을 추가한 뒤에는 application.properties에 연동할 데이터 베이스의 정보를 작성해야 합니다. 이 설정 없이는 스프링 부트 애플리케이션이 실행되지 않습니다.
// 데이터 베이스 연동
spring.datasource.driverClassName=org.mariadb.jdbc.Driver // 연동하려는 드라이버 정의
spring.datasource.url=jdbc:mariadb://localhost:3307/springboot // 마리아 DB의 경로와 데이터베이스명
spring.datasource.username=root // 설치한 계정 정보
spring.datasource.password=비밀번호 // 설치한 계정 정보
// 하이버네이트를 사용할 때 활성화할 수 있는 선택사항
spring.jpa.hibernate.ddl-auto=create // 데이터베이스를 자동으로 조작하는 옵션
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
위의 4개의 줄은 데이터 베이스를 연동하는 설정으로, 연동하려는 드라이버의 종류와 계정 정보를 기입하고 있습니다. 계정 정보는 보안상 암호화하여 사용하지만, 간편한 실습을 위해 암호화는 생략하겠습니다.
아래의 3개의 줄은 하이버네이트에 관련한 선택적 설정들입니다. ddl-auto에서 create 옵션을 통해 애플리케이션이 가동될 때마다 기존의 테이블을 지우고 새로 생성할 수 있습니다. 이 외에도 다양한 옵션이 존재합니다.
운영 환경에서는 대체로 validate나 none를 사용하고, 개발 환경에서는 create나 update를 사용하는 편입니다.
: 반복되는 코드 작성을 생략하는 방법
롬복(Lombok)은 데이터 클래스를 생성할 때 반복적으로 사용하는 메서드를 어노테이션으로 대체하는 기능을 제공하는 라이브러리입니다. 롬복을 사용하면 다음과 같은 장점이 있습니다.
- 어노테이션 기반으로 코드를 자동 생성하여 생산성이 높아집니다.
- 반복되는 코드를 생략할 수 있어 가독성이 높아집니다.
- 롬복을 안다면 코드를 유추할 수 있어 유지보수에 용이합니다.
저희는 설치단계에서 롬복을 의존성에 추가해둔 상태입니다. pom.xml 파일에 다음과 같은 코드가 추가되어 있는 것을 알 수 있습니다.
<dependencies>
...
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
...
</dependencies>
롬복에서 제공하는 어노테이션은 다음과 같습니다.
JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스로, 엔티티를 통해 Spring Data JPA에서는 데이터베이스에 테이블을 생성하기 위해 직접 쿼리를 작성할 필요가 없습니다.
엔티티 작성 시에는 어노테이션을 많이 사용하는데, 기본적으로 많이 사용되는 어노테이션을 소개하겠습니다.
@Entity
해당 클래스가 엔티티임을 명시하기 위한 어노테이션입니다. 클래스는 테이블과 일대일로 매칭되며, 해당 클래스의 인스턴스는 테이블의 레코드 한 개를 의미합니다.
@Table
엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 이 어노테이션이 필요하지 않습니다. 이 어노테이션은 테이블과 클래스의 이름을 다르게 지정할 때 사용합니다. Table(name = 값)의 형태로 데이터베이스의 테이블명을 따로 명시할 수 있습니다.
@Id
이 어노테이션이 붙은 필드는 테이블의 기본값 역할로 사용됩니다. 따라서 모든 엔티티는 @Id 어노테이션이 필요합니다.
@GeneratedValue
일반적으로 @Id 어노테이션과 함께 사용됩니다. 이 어노테이션은 해당 필드의 값을 어떤 방식으로 자동으로 생성할지 결정할 때 사용합니다. 사용하지 않는 경우, 애플리케이션에서 고유한 기본값을 생성합니다.
AUTO를 사용하는 경우, 데이터베이스에 맞게 자동 생성합니다.
IDENTITY를 사용하는 경우, 기본값 생성을 데이터 베이스에 위임합니다. 데이터베이스의 AUTO_INCREMENT를 사용하여 기본값을 생성합니다.
SEQUENCE는 @SequenceGenerator 어노테이션으로 식별자 생성기를 설정하고, 이를 통해 자동 주입받습니다.
TABLE은 어떤 DBMS를 사용해도 동일하게 동작하며, 식별자로 사용할 숫자의 보관 테이블을 별도로 생성하여 엔티티 생성시마다 값을 갱신하며 사용합니다.
@Column
이 필드는 자동으로 클래스의 필드와 테이블 칼럼을 매핑합니다.
@Transient
엔티티 클래스에는 선언돼 있지만, 데이터베이스에서는 필요 없을 경우 이 어노테이션을 사용하여 데이터베이스에서 이용하지 않게 할 수 있습니다.
위의 어노테이션을 사용하여 Product 엔티티를 생성해봅시다. 데이터베이스 테이블은 다음과 같습니다.
상품 테이블 | |
상품 번호 | int |
상품 이름 | varchar |
상품 가격 | int |
상품 재고 | int |
상품 생성 일자 | DateTime |
상품 정보 변경 일자 | DateTime |
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name="product")
@Getter
@Setter
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 createAt;
private LocalDateTime updateAt;
}
Spring Data JPA는 JpaRepository를 기반으로 더욱 쉽게 데이터베이스를 사용할 수 있는 아키텍처를 제공합니다. 스프링 부트로 JpaRepository를 상속하는 인터페이스를 생성하면, 인터페이스 자체만으로 다양한 메서드를 손쉽게 활용할 수 있습니다.
public interface ProductRepository extends JpaRepository<Product, Long> { }
JpaRepositiory를 상속받기 위해, 대상 엔티티와 기본값 타입을 지정해야 합니다. 이렇게 상속을 받으면, 별도의 메서드 구현 없이 명명규칙을 이용해 기본적인 CURD 기능을 수행할 수 있습니다.
리포지토리 메서드의 생성 규칙
자세한 쿼리 메서드는 7장에서 다루겠습니다.
DAO(Data Access Object)는 데이터베이스에 접근하기 위한 로직을 관리하는 객체입니다. 비즈니스 로직의 동작 과정에서 데이터를 조작하는 기능은 DAO 객체가 수행합니다. 다만 스프링 데이터 JPA에서 DAO의 개념은 리포지토리가 대체하고 있습니다.
규모가 작은 서비스에서는 DAO를 별도로 설계하지 않고 바로 서비스 레이어에서 데이터베이스에 접근하기도 하지만, 이번 장에서는 DAO를 서비스 레이어와 리포지토리의 중간 계층을 구성하는 역할로 사용할 예정입니다.
먼저 인터페이스를 구성합니다. 기본적인 CRUD를 다루기 위해 다음과 같이 메서드를 정의합니다.
// data/dao/ProductDAO.java
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 객체를 사용합니다.
그 다음으로는 인터페이스의 구현체를 만들겠습니다.
// data/dao/impl/ProductDAOImpl.java
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 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;
}
@Override
public Product insertProduct(Product product) {
Product savedProduct = productRepository.save(product);
return savedProduct;
}
@Override
public Product selectProduct(Long number) {
Product savedProduct = productRepository.getById(number);
return savedProduct;
}
@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.setUpdateAt(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 클래스를 스프링이 관리하는 빈으로 등록하기 위해 @Component 또는 @Service 어노테이션을 지정해야, 다른 인터페이스가 인터페이스로 의존성을 주입받을 때 이 구현체를 찾아 주입하게 됩니다.
insertProduct() 메서드는 Product 엔티티를 데이터베이스에 저장하는 기능을 수행합니다. 리포지토리를 생성할 때, 인터페이스에서 따로 메서드를 구현하지 않아도, JPA에서 기본 메서드를 제공하므로 save 메서드를 활용하여 작성할 수 있습니다.
selectProduct() 메서드는 조회메서드로, 리포지토리의 메서드 getById()를 이용하였습니다.
updateProductName() 메서드는 Product 데이터의 상품명을 업데이트하는 기능을 합니다. JPA에서 데이터의 값을 변경할 때는 다른 메서드와 다른 점이 있습니다. JPA는 값을 갱신할 때 update라는 키워드를 사용하지 않습니다. 영속성 컨텍스트를 활용해 값을 갱신하는데, find() 메서드를 통해 데이터 베이스에서 값을 가져오면 가져온 객체가 영속성 컨텍스트에 추가됩니다. 영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 save()를 실행하면, JPA는 더티체크라고 하는 변경 감지를 수행합니다. 변경이 감지되면, 대상 객체에 해당하는 데이터베이스의 레코드를 업데이트하는 쿼리가 실행됩니다.
다음으로는 삭제 메서드인 deleteProduct() 메서드를 구현합니다. 레코드를 삭제하기 위해, findById() 메서드를 통해 삭제하려는 레코드와 매핑된 영속 객체를 영속성 컨텍스트에 가져오는 작업을 수행하고, delete() 메서드를 통해 해당 객체를 삭제합니다.
위에서 설계한 구성 요소들을 클라이언트의 요청과 연결하려면 컨트롤러와 서비스를 생성해야 합니다.
이를 위해 먼저 DAO의 메서드를 호출하고 그 외 비즈니스 로직을 수행하는 서비스 레이어를 생성한 후, 컨트롤러를 생성하겠습니다.
서비스 레이어에서는 도메인 모델을 활용해 애플리케이션에서 제공하는 핵심 기능을 제공합니다.
서비스 인터페이스를 작성하기 전, 필요한 DTO 클래스를 생성하겠습니다.
// data/dto/ProductDto.java
package com.springboot.jpa.data.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
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;
}
}
// data/dto/ProductResponseDto.java
package com.springboot.jpa.data.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
}
그리고 서비스 인터페이스를 작성합니다. 기본적인 CRUD 기능을 호출하기 위해 간단히 메서드를 정의하겠습니다.
// service/ProductService.java
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;
}
오버라이딩된 메서드를 구현하겠습니다.
// service/impl/ProductServiceImpl.java
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 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;
}
@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.setUpdateAt(LocalDateTime.now());
product.setCreateAt(LocalDateTime.now());
Product savedProduct = productDAO.insertProduct(product);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setStock(savedProduct.getStock());
productResponseDto.setName(savedProduct.getName());
productResponseDto.setNumber(savedProduct.getNumber());
productResponseDto.setPrice(savedProduct.getPrice());
return productResponseDto;
}
@Override
public ProductResponseDto changeProductName(Long number, String name) throws Exception{
Product changeProduct = productDAO.updateProductName(number, name);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setNumber(changeProduct.getNumber());
productResponseDto.setName(changeProduct.getName());
productResponseDto.setStock(changeProduct.getStock());
productResponseDto.setPrice(changeProduct.getPrice());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) throws Exception{
productDAO.deleteProduct(number);
}
}
현재 서비스 레이어에는 DTO 객체와 엔티티 객체가 공존하여 변환 작업이 필요합니다. 현재 코드에서는 DTO 객체를 생성하고 값을 넣어 초기화하는데, 이런 부분은 빌드 패턴을 활용하거나 엔티티 객체나 DTO 객체 내부에 변환하는 메서드를 추가해 간단하게 전환할 수 있습니다.
조회 메서드, 저장 메서드, 업데이트 메서드, 삭제 메서드를 구현한 상태입니다.
저장하는 saveProduct()는 전달받은 DTO객체를 통해 엔티티 객체를 생성해서, 초기화 한 후 DAO 객체로 전달하면 됩니다. 일반적으로 저장 메서드는 void 타입이나 boolean 타입으로 지정하는 경우가 많은데, 리턴 타입은 해당 비즈니스 로직이 어떤 성격을 띠느냐에 따라 결정하는 것이 바람직합니다.
업데이트 메서드에 해당하는 changePrdouctName() 메서드는 다음과 같은 DTO가 필요합니다. 이름을 변경하기 위해 대상을 식별할 수 있는 인덱스 값과 변경하려는 이름을 받아옵니다. 좀 더 견고하게 코드를 작성하기 위해 기존 이름도 받아와 상품 정보와 일치하는 검증 단계를 추가하기도 합니다.
서비스 객체의 설계를 마친 후에는 비즈니스 로직과 클라이언트 요청을 연결하는 컨트롤러를 생성해야 합니다.
또한 컨트롤러에서 사용할 ChangeProductNameDtro도 생성합니다.
// data/dto/ChangeProductNameDto.java
package com.springboot.jpa.data.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChangeProductNameDto {
private Long number;
private String name;
}
// controller/ProductController.java
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("정상적으로 삭제되었습니다.");
}
}
이제 Swagger API를 통해 클라이언트의 입장에서 기능을 요청하고, 결과를 살펴보겠습니다.
Swagger API를 사용하기 위해 애플리케이션을 실행하고, 웹 브라이저를 통해 Swagger 페이지로 접속합니다.
상품 정보 저장
저장을 위해 POST 메서드에서 createProduct 메서드를 사용합니다. POST API에서 [Try it out]을 눌러 위의 사진처럼 값을 입력합니다.
[Execute] 버튼을 누르면 다음과 같은 하이버네이트 로고를 볼 수 있습니다.
HeidiSQL을 통해 데이터베이스에 값이 잘 저장된 것을 확인할 수 있습니다.
상품 정보 조회
getProduct에서 조회하고자 하는 number 칼럼의 값을 입력하면 아래와 같이 응답 Body에 값이 담긴 것을 확인할 수 있습니다.
상품 정보 변경
updateProductName() 메서드를 통해 상품 이름을 변경하겠습니다.
Swagger에서 Boby에 식별자 번호와 바꾸고자 하는 이름을 기입한 후, [Execute] 버튼을 클릭하면 다음과 같이 변경된 상태의 값이 응답으로 오는 것을 확인할 수 있습니다.
상품 정보 삭제
삭제하려는 number값을 파라미터에 입력하면 다음과 같이 결과 화면을 확인할 수 있습니다.
1. ddl-auto 옵션을 대체로 운영 환경에서는 ( validate )나 ( none )를 사용하고, 개발 환경에서는 ( create )나 ( update )를 사용하는 편이다.
2. 롬복에서 제공하는 어노테이션은 getter/setter 메서드를 생성하는 ( @Getter ), ( @Setter ), 매개변수 없는 생성자를 자동으로 생성하는 ( @NoArgsConstructor ), @모든 필드를 매개변수로 갖는 생성자를 자동으로 생성하는 ( @AllArgsConstructor ), 필드 중 final 이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동으로 생성하는 ( @RequiredArgsConstructor ) 등이 있다.
3. ( @Transient ) 어노테이션을 사용하여, 엔티티 클래스에서는 선언하지만 데이터베이스에서 이용하지 않게 할 수 있다.
4. ( JpaRepository )를 상속하는 인터페이스를 생성하면, 인터페이스 자체만으로 다양한 메서드를 손쉽게 활용할 수 있다.
5. 일반적인 설계 원칙에서는 ( 엔티티 객체 )는 데이터 베이스에 접근하는 계층에서만 사용하도록 정의하고, 다른 계층으로 데이터를 전달할 때는 ( DTO 객체 )를 사용한다.
6. 영속성 컨텍스트가 유지되는 상황에서 객체의 값을 변경하고 다시 ( save() )를 실행하면, JPA는 ( 더티체크 )라고 하는 변경 감지를 수행한다. 변경이 감지되면, 대상 객체에 해당하는 데이터베이스의 레코드를 ( 업데이트 )하는 쿼리가 실행된다.
7. 클래스를 스프링이 관리하는 빈으로 등록하기 위해 ( @Component ) 또는 ( @Service ) 어노테이션을 지정해야, 다른 인터페이스가 인터페이스로 의존성을 주입받을 때 이 구현체를 찾아 주입하게 된다.
8. 다음과 같은 멤버 테이블을 만드세요. 기본값은 데이터베이스에 맞게 자동 생성되도록 하고, 기본값의 필드명은 'member_id'입니다. . 전공은 null값이 들어갈 수 없으며, 엔티티 값은 외부에서 변경할 수 없습니다.
멤버 | |
이름 | String |
학번 | int |
전공 | String |
생일 | LocalDate |
9. JpaRepository를 상속받아 멤버 리포지토리를 만드세요. 리포지토리에 기본값으로 멤버를 조회하는 인터페이스와 전공으로 멤버들의 수를 반환하는 인터페이스를 작성하세요.
정답
8.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="member_id")
private Long id;
private String name;
private int number;
@Column(nullable = false)
private String major;
private LocalDate BirthDay;
}
9.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 1번
Optional<Member> findById(Long memberId);
// 2번
Long countByMajor(String major);
}
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.104-158.
Corner Spring 2
Editor : 이조
[스프링2] 8장. Spring Data JPA 활용 (0) | 2023.11.24 |
---|---|
[스프링2] 7장. 테스트 코드 작성하기 (0) | 2023.11.17 |
[스프링2] 5-6장. API를 작성하는 다양한 방법 & 데이터베이스 연동 (0) | 2023.11.03 |
[스프링2] 1-4장. 스프링 부트란? & 개발에 앞서 알면 좋은 기초 지식 & 개발 환경 구성 & 스프링 부트 애플리케이션 개발하기 (0) | 2023.10.13 |
[스프링2] 섹션 6. 스프링 DB 접근 기술 (0) | 2023.10.06 |