

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 속성을 가진 객체, 단순한 값 타입 표현을 위한 클래스
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();
}
}
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();
}
}
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> {
}
package studio.thinkground.testproject.data.dao;
import studio.thinkground.testproject.data.entity.ProductEntity;
public interface ProductDAO {
ProductEntity saveProduct(ProductEntity product);
ProductEntity getProduct(String productID);
}
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;
}
}
Java의 데이터 클래스와 RDB의 테이블 매핑
OOP 프로그래밍과 RDB의 차이로 발생하는 제약사항을 해결해주는 역할 (JPA, Hibernate)
PROS SQL 쿼리가 아닌 직관적 코드로 데이터 조작, 재사용, 유지보수, DBMS 종속성 감소
CONS 복잡성이 커질 경우 ORM으로 구현하기 어려움, 속도 저하, 대형 쿼리 튜닝 필요
ORM과 관련된 인터페이스들의 모음
Java 진영에서의 표준 ORM, JPA는 ORM보다 더 구체적인 스펙을 포함하고 있음
ORM Framework 중 하나, JPA의 실페 구현체 중 하나, 가장 많이 사용됨
Spring Framework에서 JPA를 편리하게 사용할 수 있도록 지원하는 Lib
CRUD 처리용 인터페이스 제공, Repository 개발 시 인터페이스만 작성하면 구현 객체를 동적으로 생성하여 주입, 데이터 접근 계층 개발 시 인터페이스만 작성하면 됨
Hibernate에서 자주 사용되는 기능을 조금 더 쉽게 사용할 수 있도록 구현
인터페이스와 원본 클래스를 나누어서 클래스를 설계하는 것
클래스들과 프로젝트의 의존성을 낮추는 역할을 하게 되는 것
ex. 다른 DB를 사용하려고 할 때 같은 기능을 할 수 있도록 만드는 것
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);
}
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 단에서 변환하는 것도 가능하다
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
Log4J를 기반으로 개발된 로깅 라이브러리, 빠른 퍼포먼스, 메모리 효율 증대
로그의 특정 레벨을 설정할 수 있음 (Trace -> Debug -> Info -> Warn -> Error)
프로덕션, 테스트 환경에서 각각 다른 출력 레벨을 설정하여 로그를 확인할 수 있음
출력 방식에 대해 설정할 수 있음, 별도의 프로그램 없이 자체적으로 로그 압축, 보관 기간 설정
일반적으로 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>
Log의 형태 및 출력 위치를 설정하기 위한 영역
ConsoleAppender, FileAppender(파일 저장), RollingFileAppender(파일 순회하며 저장)
appender 내에 포함되는 항목, pattern을 사용하여 원하는 형식으로 로그 표현
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger %msg%n</pattern>
</encoder>
설정한 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>
Trace > Debug > Info > Warn > Error
DEBUG 레벨보다 더 디테일한 메세지를 표현하기 위한 레벨DEBUG 로그에서 모두 처리
서비스의 비즈니스 로직이 올바르게 동작하기 위해 사용되는 데이터에 대해 유효성 검사 필요
데이터 검증은 여러 계층에서 발생, 들어오는 데이터에 대해 의도한 형식이 들어오는지 검사
어플리케이션 전체적으로 분산되어 존재, 코드의 중복, 검사 추적 어려움
이러한 문제를 해결하기 위해 Bean Validation, Hibernate Validator 출시
Spring Boot의 유효성 검사 표준은 Hibernate Validator를 채택

...
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;
...
@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 처럼 수동으로 조금 더 자세한 유효성 검증이 가능하다