Introduction to Spring Boot - 3

SangYeon Min·2024년 3월 10일

STUDY-SPRING-BOOT

목록 보기
3/8
post-thumbnail

DB Appliance

SpringBoot Service Architecture

Entitiy : Domain, DB에 쓰일 컬럼과 여러 엔티티 연관관계 정의, 데이터베이스 테이블을 하나의 엔티티로 생각해도 무방함, 해당 클래스의 필드는 각 테이블 내부의 Column 의미

Repository : Entitiy에 의해 생성된 DB에 접근하는 메소드를 사용하기 위한 인터페이스, Service와 DB를 연결하는 고리 역할 수행, DB에 적용하고자 하는 CRUD를 정의하는 영역

DAO : DataAccessObject, DB에 접근하기 위한 객체, 직접 DB에 접근, Peristent Layer, Service가 DB에 연결할 수 있게 해주는 역할, DB를 사용하여 데이터 조회하거나 접근

DTO : DataTransferObject 계층간 데이터 교환을 위한 객체
VO : ValueObject, Read-Only 속성을 가진 객체, 단순한 값 타입 표현을 위한 클래스

/data/dto/ProductDTO.java

package studio.thinkground.testproject.dto;

import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ProductDTO {
    
    private String productID;
    private String productName;
    private int productPrice;
    private int productStack;
    
    public ProductEntity toEntity(){
        return ProductEntity.builder()
                .productID(productID)
                .productName(productName)
                .productPrice(productPrice)
                .productStack(productStack)
                .build();
    }
}

/data/entity/ProductEntity.java

package studio.thinkground.testproject.data.entity;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import studio.thinkground.testproject.data.dto.ProductDTO;

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
@Table(name = "product")
public class Product extends BaseEntity{

    @Id
    String id;
    String name;
    Integer price;
    Integer stock;

  /*
  @Column
  String sellerId;

  @Column
  String sellerPhoneNumber;
   */

    public ProductDTO toDto(){
        return ProductDTO.builder()
                .productID(id)
                .productName(name)
                .productPrice(price)
                .productStock(stock)
                .build();
    }

}

/data/repository/ProductRepository.java

package studio.thinkground.testproject.data.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import studio.thinkground.testproject.data.entity.ProductEntity;

public interface ProductRepository extends JpaRepository<ProductEntity, String> {

}

/data/dao/ProductDAO.java

package studio.thinkground.testproject.data.dao;

import studio.thinkground.testproject.data.entity.ProductEntity;

public interface ProductDAO {

    ProductEntity saveProduct(ProductEntity product);

    ProductEntity getProduct(String productID);

}

/data/dao/impl/ProductDAOImpl.java

@Autowired

Spring은 Singleton을 기반으로 작동한다
하나의 객체를 여러 곳에서 사용하는 형식이기 때문에 해당 어노테이션으로 끌어와서 주입

package studio.thinkground.testproject.data.dao.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import studio.thinkground.testproject.data.dao.ProductDAO;
import studio.thinkground.testproject.data.entity.ProductEntity;
import studio.thinkground.testproject.data.repository.ProductRepository;

@Service
public class ProductDAOImpl implements ProductDAO {

    ProductRepository productRepository;

    @Autowired
    public ProductDAOImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public ProductEntity saveProduct(ProductEntity productEntity) {
        productRepository.save(productEntity);
        reuturn productEntity;
    }

    @Override
    public ProductEntity getProduct(String productId) {
        ProductEntity productEntity = productRepository.getByID(productId);
        return productEntity;
    }

}

ORM & JPA

Object Relational Mapping

Java의 데이터 클래스와 RDB의 테이블 매핑
OOP 프로그래밍과 RDB의 차이로 발생하는 제약사항을 해결해주는 역할 (JPA, Hibernate)
PROS SQL 쿼리가 아닌 직관적 코드로 데이터 조작, 재사용, 유지보수, DBMS 종속성 감소
CONS 복잡성이 커질 경우 ORM으로 구현하기 어려움, 속도 저하, 대형 쿼리 튜닝 필요

Java Persistent API

ORM과 관련된 인터페이스들의 모음
Java 진영에서의 표준 ORM, JPA는 ORM보다 더 구체적인 스펙을 포함하고 있음

Hibernate

ORM Framework 중 하나, JPA의 실페 구현체 중 하나, 가장 많이 사용됨

Spring Data JPA

Spring Framework에서 JPA를 편리하게 사용할 수 있도록 지원하는 Lib
CRUD 처리용 인터페이스 제공, Repository 개발 시 인터페이스만 작성하면 구현 객체를 동적으로 생성하여 주입, 데이터 접근 계층 개발 시 인터페이스만 작성하면 됨
Hibernate에서 자주 사용되는 기능을 조금 더 쉽게 사용할 수 있도록 구현

Loose Coupling

인터페이스와 원본 클래스를 나누어서 클래스를 설계하는 것
클래스들과 프로젝트의 의존성을 낮추는 역할을 하게 되는 것
ex. 다른 DB를 사용하려고 할 때 같은 기능을 할 수 있도록 만드는 것

/service/ProductService.java

package studio.thinkground.aroundhub.service;

import studio.thinkground.aroundhub.data.dto.ProductDto;

public interface ProductService {

  ProductDto saveProduct(String productId, String productName, int productPrice, int productStock);

  ProductDto getProduct(String productId);

}

/service/impl/ProductServiceImpl.java

package studio.thinkground.aroundhub.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import studio.thinkground.aroundhub.data.dto.ProductDto;
import studio.thinkground.aroundhub.data.entity.Product;
import studio.thinkground.aroundhub.data.handler.ProductDataHandler;
import studio.thinkground.aroundhub.service.ProductService;

@Service
public class ProductServiceImpl implements ProductService {

    ProductDataHandler productDataHandler;

    @Autowired
    public ProductServiceImpl(ProductDataHandler productDataHandler) {
        this.productDataHandler = productDataHandler;
    }

    @Override
    public ProductDto saveProduct(String productId, String productName, int productPrice,
        int productStock) {

        Product product = productDataHandler.saveProductEntity(productId, productName,
            productPrice, productStock);

        ProductDto productDto = new ProductDto(product.getId(),
            product.getName(), product.getPrice(),
            product.getStock());

        return productDto;
    }

    @Override
    public ProductDto getProduct(String productId) {

        Product product = productDataHandler.getProductEntity(productId);
        ProductDto productDto = new ProductDto(product.getId(),
            product.getName(), product.getPrice(),
            product.getStock());

        return productDto;
    }
}

DTO, Entitiy는 Service 계층에서 변환하는 것이 일반적
but 간단한 프로젝트의 경우에서는 Controller 단에서 변환하는 것도 가능하다

application.yaml

mariadb와 jpa 설정을 application.yaml을 통해 프로젝트에 적용한다

spring:
  datasource:
    driverClassName: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3308/springboot
    username: ENC(LdsY1or+HixRvd/oLzeD0Q==)
    password: ENC(ocjxUpU0Aq6GM0WtxG9zhQEq7RlpTQey)
  jpa:
    hibernate.ddl-auto: none
    show-sql: true

Logback

Log4J를 기반으로 개발된 로깅 라이브러리, 빠른 퍼포먼스, 메모리 효율 증대
로그의 특정 레벨을 설정할 수 있음 (Trace -> Debug -> Info -> Warn -> Error)
프로덕션, 테스트 환경에서 각각 다른 출력 레벨을 설정하여 로그를 확인할 수 있음
출력 방식에 대해 설정할 수 있음, 별도의 프로그램 없이 자체적으로 로그 압축, 보관 기간 설정

Logback 설정

일반적으로 Classpath에 있는 logback 설정 파일 참조
아래 코드와 같이 {} 이후 , 뒤에 출력할 데이터를 입력할 수 있음

LOGGER.info("[saveProductEntity] productDAO로 Product 정보 저장 요청. productId : {}", productId);
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds"> <!-- logback 구현체가 설정을 확인하는 주기 -->
  <property name="moduleId" value="around_hub_spring_boot"/><!-- moduleId는 프로젝트가 설치된 폴더명 또는 구분할 수 있는 식별자이면 됨 -->
  <property name="type" value="around_hub"/><!-- 로그파일명을 구성하는 인자 -->
  <property name="logback" value="logback"/><!-- log를 저장할 최종 디렉토리명 -->
  <property name="logdir" value="D:\Workspace\LogFiles"/>

  <!-- Colors -->
  <!-- %black", "%red", "%green", "%yellow", "%blue", "%magenta",
  "%cyan", "%white", "%gray", "%boldRed", "%boldGreen", "%boldYellow",
  "%boldBlue", "%boldMagenta", "%boldCyan", "%boldWhite" and "%highlight" -->

  <!-- Appenders -->
  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>DEBUG</level>
    </filter>
    <encoder>
      <pattern>%green([%d{yyyy-MM-dd HH:mm:ss.SSS}]) %magenta([%-5level]) %highlight([%thread]) %cyan(%logger{30}) %yellow(%msg%n)</pattern>
    </encoder>
  </appender>


  <appender name="DEBUG_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>DEBUG</level>
    </filter>
    <file>${logdir}/${moduleId}/${logback}/debug_${type}.log</file>
    <append>true</append>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${logdir}/${moduleId}/${logback}/debug_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>INFO</level>
    </filter>
    <file>${logdir}/${moduleId}/${logback}/info_${type}.log</file>
    <append>true</append>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${logdir}/${moduleId}/${logback}/info_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
    </encoder>
  </appender>


  <appender name="WARN_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>WARN</level>
    </filter>
    <file>${logdir}/${moduleId}/${logback}/warn_${type}.log</file>
    <append>true</append>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${logdir}/${moduleId}/${logback}/warn_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="ERROR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>WARN</level>
    </filter>
    <file>${logdir}/${moduleId}/${logback}/error_${type}.log</file>
    <append>true</append>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${logdir}/${moduleId}/${logback}/error_${type}.%d{yyyy-MM-dd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
    </encoder>
  </appender>

  <!-- TRACE > DEBUG > INFO > WARN > ERROR > OFF -->
  <!-- Root Logger -->
  <root level="INFO">
    <appender-ref ref="console" />
    <!--
<appender-ref ref="DEBUG_LOG" />
<appender-ref ref="INFO_LOG" />
<appender-ref ref="WARN_LOG" />
<appender-ref ref="ERROR_LOG" />
-->
  </root>
</configuration>

appender

Log의 형태 및 출력 위치를 설정하기 위한 영역
ConsoleAppender, FileAppender(파일 저장), RollingFileAppender(파일 순회하며 저장)

encoder

appender 내에 포함되는 항목, pattern을 사용하여 원하는 형식으로 로그 표현

<encoder>
  <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
</encoder>

root

설정한 appender를 참조하여 로그의 레벨을 설정
root : 전역 설정, logger : 지역 설정

<root level="INFO">
    <appender-ref ref="console" />
    <!--
<appender-ref ref="DEBUG_LOG" />
<appender-ref ref="INFO_LOG" />
<appender-ref ref="WARN_LOG" />
<appender-ref ref="ERROR_LOG" />
-->
  </root>

Log Level

Trace > Debug > Info > Warn > Error
  1. Trace : DEBUG 레벨보다 더 디테일한 메세지를 표현하기 위한 레벨
  2. DEBUG : 애플리케이션 디버깅을 위한 메세지 레벨, 보통 DEBUG 로그에서 모두 처리
  3. INFO : 상태변경과 같은 정보성 메세지
  4. WARN : 시스템의 에러 원이이 될 수 있는 경고 레벨, 처리 가능한 사항
  5. ERROR : 로직 수행 중에 오류가 발생한 경우, 시스템적으로 심각한 문제가 발생
    로그 레벨을 지정했을 경우 하위 레벨은 출력하지 않음

pattern


Validation

서비스의 비즈니스 로직이 올바르게 동작하기 위해 사용되는 데이터에 대해 유효성 검사 필요
데이터 검증은 여러 계층에서 발생, 들어오는 데이터에 대해 의도한 형식이 들어오는지 검사

일반적인 Validation의 문제점

어플리케이션 전체적으로 분산되어 존재, 코드의 중복, 검사 추적 어려움
이러한 문제를 해결하기 위해 Bean Validation, Hibernate Validator 출시
Spring Boot의 유효성 검사 표준은 Hibernate Validator를 채택

Validation 관련 Annotation

/dto/ProductDTO.java

...
public class ProductDto {

  //@Size(min = 8, max = 8) // abcdefg
  @NotNull
  private String productId;

  @NotNull
  @Id
  private String productName;

  @NotNull
  @Min(value = 500)
  @Max(value = 3000000)
  private int productPrice;

  @NotNull
  @Min(value = 0)
  @Max(value = 9999)
  private int productStock;

/controller/ProductController.java

...
	@PostMapping(value = "/product")
    public ResponseEntity<ProductDto> createProduct(@Valid @RequestBody ProductDto productDto) {

        LOGGER.info("[createProduct] perform {} of Around Hub API.", "createProduct");

        // Validation Code Example
        if (productDto.getProductId().equals("") || productDto.getProductId().isEmpty()) {
            LOGGER.error("[createProduct] failed Response :: productId is Empty");
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(productDto);
        }
        
        // Validation Code Example
        if (productDto.getProductId().equals("") || productDto.getProductId().isEmpty()) {
            LOGGER.error("[createProduct] failed Response :: productId is Empty");
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(productDto);
        }
        ...

@Valid Annotation을 추가하여 DTO에 대한 유효성 검증 가능
Validation Code Example 처럼 수동으로 조금 더 자세한 유효성 검증이 가능하다


자료 출처
https://www.youtube.com/@around.hub.studio

0개의 댓글