6-3. 데이터베이스 연동[Spring Boot]

JuJaeng2·2023년 12월 11일

이번 글에서는 6-2글에 이어서 컨트롤러와 서비스를 만들어보도록 할 것이다.

✅ DAO 연동을 위한 컨트롤러와 서비스 설계

✳️ 서비스 생성

컨트롤러를 만들기 전에 비즈니스 로직을 수행하는 서비스 레이어를 생성하도록 한다.

  • 서비스 레이어에서는 애플리케이션에서 제공하는 핵심 기능을 제공한다.
  • 핵심 기능을 구현하려면 세부 기능을 정의해야한다.
  • 이를 위해 아키텍처를 서비스 로직과 비즈니스 로직으로 분리하기도 한다.

비즈니스 로직 vs 서비스 로직
비즈니스 로직은 특정 비즈니스를 해결하기 위한 로직을 말한다. 예를들어서 회원가입 시 중복 체크, 상품구매시 재고량 확인 등을 말한다. 이때 비즈니스 로직에 사용되는 것이 서비스 로직이다. 비즈니스 로직에 필요한 핵심 기능들을 서비스로직이 구현하고 이를 비즈니스 로직에서 사용한다.

하지만 현재는 과도한 기능구현은 하지 않을 것이므로 서비스 레이어에서 비즈니스 로직을 처리하는 아키텍처로 구성을 할 것이다.
서비스 객체 역시 DAO처럼 추상화해서 구성한다.

데이터를 받을 ProductDto 클래스응답할 때 데이터를 담아서 보낼 ProductResponseDto 클래스를 생성합니다.

ProductDto

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 메서드 생략
}

ProductResponseDto

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 생략
}

이제 서비스 인터페이스를 작성하고 구현부를 작성합니다

ProductService Interface

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;
}

ProductServiceImpl

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를 통한 동작 확인

Swagger API를 사용해서 애플리케이션의 클라이언트 입장에서 기능을 요청해보고 어떻게 결과가 나타나는지 확인해 보도록 하겠다.

Swagger 라이브러리 추가

build.gralde파일에 swagger 라이브러리를 추가해 준다.

Spring Boot 3버전 부터는 springfox가 적용되지 않아서 springdoc을 사용했다.

    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

application.yml 작성

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

Swagger 설정

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 수정

각 메서드에 대해 설명을 명시학 위해 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를 사용할 수 있다.

테스트해보기

createProduct로 Product 만들기

getProduct로 Product정보 가져오기

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

방금 만든 TestProduct의 값을 잘 가져온것을 확인할 수 있다.

changeProduct로 Product의 이름 변경하기

TestProduct를 changedName 변경해보도록 한다.
변경할 Product의 number와 변경할 name을 담아서 요청을 보낸다.

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

deleteProduct로 Product 삭제하기


마무리 😁

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

  • 내가 지금까지 사용한 JPA는 하이버네이트를 쉽게 사용할 수 있도록 해준 Spring Data JPA였다.
  • DB에는 객체 개념이 없어 자바에서의 객체와의 연결을 도와주는 엔티티 매니저가 있고 이는 엔티티 매니저 팩토리에서 만든다.
  • 데이터를 업데이트 할 때는 영속성 컨택스트에 들어온 엔티티를 더티체크하여 영속성 객체의 변화를 감지하고 SQL문의 update쿼리를 실행시킨다.
  • 데이터를 주고받을때는 DTO객체를 사용하여 주고받고 데이터베이스에서는 DAO를 사용하여 전달한다.
  • 비즈니스 로직과 서비스 로직은 다른 개념이며 서비스로직이 더 하위개념이고 구체화된 개념이다. 서비스로직을 만들어서 비즈니스 로직의 핵심기능을 구성하는 것이다.
profile
다 잘하고 싶은 개발자

0개의 댓글