이번 글에서는 6-2글에 이어서 컨트롤러와 서비스를 만들어보도록 할 것이다.
컨트롤러를 만들기 전에 비즈니스 로직을 수행하는 서비스 레이어를 생성하도록 한다.
❓비즈니스 로직 vs 서비스 로직
비즈니스 로직은 특정 비즈니스를 해결하기 위한 로직을 말한다. 예를들어서 회원가입 시 중복 체크, 상품구매시 재고량 확인 등을 말한다. 이때 비즈니스 로직에 사용되는 것이 서비스 로직이다. 비즈니스 로직에 필요한 핵심 기능들을 서비스로직이 구현하고 이를 비즈니스 로직에서 사용한다.
하지만 현재는 과도한 기능구현은 하지 않을 것이므로 서비스 레이어에서 비즈니스 로직을 처리하는 아키텍처로 구성을 할 것이다.
서비스 객체 역시 DAO처럼 추상화해서 구성한다.
데이터를 받을 ProductDto 클래스와 응답할 때 데이터를 담아서 보낼 ProductResponseDto 클래스를 생성합니다.
package com.database.databasebookstudy.data.dto;
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;
}
//Getter, Setter 메서드 생략
}
package com.database.databasebookstudy.data.dto;
public class ProductResponseDto {
private Long number;
private String name;
private int price;
private int stock;
public ProductResponseDto(){}
public ProductResponseDto(Long number, String name, int price, int stock) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
}
// Getter, Setter 생략
}
이제 서비스 인터페이스를 작성하고 구현부를 작성합니다
package com.database.databasebookstudy.service;
import com.database.databasebookstudy.data.dto.ProductDto;
import com.database.databasebookstudy.data.dto.ProductResponseDto;
public interface ProductService {
ProductResponseDto getProduct(Long number);
ProductResponseDto saveProduct(ProductDto productDto);
ProductResponseDto changeProduct(Long number, String name) throws Exception;
void deleteProduct(Long number) throws Exception;
}
package com.database.databasebookstudy.service.impl;
// import문 생략
@Service
public class ProductServiceImpl implements ProductService {
private final ProductDAO productDAO;
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.setName(savedProduct.getName());
productResponseDto.setPrice(savedProduct.getPrice());
productResponseDto.setNumber(savedProduct.getNumber());
productResponseDto.setStock(savedProduct.getStock());
return productResponseDto;
}
@Override
public ProductResponseDto changeProduct(Long number, String name) throws Exception {
Product changedProduct = productDAO.updateProductName(number, name);
ProductResponseDto productResponseDto = new ProductResponseDto();
productResponseDto.setName(changedProduct.getName());
productResponseDto.setNumber(changedProduct.getNumber());
productResponseDto.setPrice(changedProduct.getPrice());
productResponseDto.setStock(changedProduct.getStock());
return productResponseDto;
}
@Override
public void deleteProduct(Long number) throws Exception {
productDAO.deleteProduct(number);
}
}
서비스 레이어에는 DTO 객체와 엔티티 객체가 공존하도록 설계되어 있어서 변환 작업이 필요합니다. 그래서 각 메서드들을 보면 DTO객체로 값을 받아 엔티티 객체로 변환하여 DAO객체에 전달하고 결과를 다시 DTO객체에 담아 응답합니다.

이제는 비즈니스 로직과 클라이언트의 요청을 연결하는 컨트롤러를 생성해야한다.
컨틀롤러는 클라이언트로부터 요청을 받고 해당 요청에 대해 서비스 레이어에 구현된 적절한 메서드를 호출해서 결과값을 받는다.
컨트롤러는 요청과 응답을 전달하는 역할만 맡는것이 좋다.
우선 컨트롤러 메서드 중 값을 업데이트하는 메서드에서 클라이언트로부터 데이터를 받을 ChangeProductNameDto를 생성해준다.
package com.database.databasebookstudy.data.dto;
public class ChangeProductNameDto {
private Long number;
private String name;
public ChangeProductNameDto(){}
public ChangeProductNameDto(Long number, String name) {
this.number = number;
this.name = name;
}
// Getter, Setter 생략
}
이제 컨트롤러를 생성해준다.
package com.database.databasebookstudy.controller;
//import 생략
@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.changeProduct(
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를 사용해서 애플리케이션의 클라이언트 입장에서 기능을 요청해보고 어떻게 결과가 나타나는지 확인해 보도록 하겠다.
build.gralde파일에 swagger 라이브러리를 추가해 준다.
Spring Boot 3버전 부터는 springfox가 적용되지 않아서 springdoc을 사용했다.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
springdoc:
packages-to-scan: com.database.databasebookstudy
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
swagger-ui:
path: /
disable-swagger-default-url: true
display-request-duration: true
operations-sorter: alpha
package com.database.databasebookstudy.configuration;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI(){
return new OpenAPI()
.components(new Components())
.info(apiInfo());
}
private Info apiInfo() {
return new Info()
.title("SpringBoot Open API Test with Swagger")
.description("데이터베이스 연동 실습 API")
.version("1.0.0");
}
}
각 메서드에 대해 설명을 명시학 위해 controller에 수정이 필요하다. 컨트롤러의 다른 코드는 동일하므로 수정된 부분만 작성했다.
@Tag(name = "Prodcut API", description = "Spring Boot API 테스트")
@RestController
@RequestMapping("/product")
public class ProductController {
@Operation(summary = "Get Product")
@Parameter(name = "number", description = "찾으려는 Product의 number")
@GetMapping
public ResponseEntity<ProductResponseDto> getProduct(Long number)
@Operation(summary = "Create Product")
@Parameter(name = "productDto", description = "새로운 Product 만들기")
@PostMapping
public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto productDto)
@Operation(summary = "Change Product Name")
@Parameter(name = "changeProductNameDto", description = "Product의 이름을 변경하기")
@PutMapping
public ResponseEntity<ProductResponseDto> changeProductName(
@RequestBody ChangeProductNameDto changeProductNameDto)
@Operation(summary = "Delete Product")
@Parameter(name = "number", description = "Product 삭제")
@DeleteMapping
public ResponseEntity<String> deleteProduct(Long number) throws Exception
}
이제 스프링부트를 실행하고 http://localhost:8080/swagger-ui/index.html로 이동하면 Swagger API를 사용할 수 있다.



방금 만든 Product의 Number가 1 이므로 number로 1을 준다.

방금 만든 TestProduct의 값을 잘 가져온것을 확인할 수 있다.
TestProduct를 changedName 변경해보도록 한다.
변경할 Product의 number와 변경할 name을 담아서 요청을 보낸다.


TestProduct가 changedName으로 바뀐것을 볼 수 있다.

이번 챕터에서는 데이터베이스를 연동하고 DAO에 JPA 레포지토리를 사용하여 데이터베이스를 조작하고 간단한 비즈니스 로직을 구현하여 CRUD작업을 해보았다. 간단하게 구현하려면 더 간단하게 할 수 있는 수준이었지만 아키텍처의 틀을 사용하면서 구현을 했다는 점에서 조금 복잡했지만 의미가 있다고 생각한다.
마지막으로 이번챕터에서 새롭게 알게된 점을 정리하며 마치도록 하겠다.