package com.portfolio.dto;


import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/*
    * API 요청 결과를 담는 DTO
    * date: 2024-07-26
    * author: 김만기
    ====== Annotation description ======
    Schema: swagger 문서 작성을 위한 annotation
 */
@Getter
@Setter
public class ApiResultDto<T> {

    @Schema(description = "결과 코드", example = "200")
    private String resultCode;
    @Schema(description = "결과 메시지", example = "성공")
    private String resultMessage;
    @Schema(description = "추가 데이터")
    private T data;
}

normal-board의 프로젝트 구조

NormalBoard 패키지구조


main 프로젝트의 application.porperites(JPA 및 DB 설정)

server.port = 8080


# JPA Configuration
spring.datasource.url=jdbc:mariadb://localhost:3306/portfolio
spring.datasource.username=adm
spring.datasource.password=123
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace

 

 


BaseEntity(테이블 공통) 및 NormalBoardEntity 

BaseEntity

package com.portfolio.entity;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

import java.time.LocalDateTime;

/*
    * date : 2024.07.26
    * author : 김만기
    * Base Entity: 모든 엔티티의 공통 필드를 정의한 추상 클래스

    * ====== Annotation description ======
    MappedSuperclass: jpa에서 상속 시 상속 받은 값들을 컬럼으로 사용하기 위함
    SuperBuilder: extends한 클레스에 builder를 사용하기 위함
    Column: 컬럼명, 길이, notNull 등의 설정

    * ====== field description ======
    firstRegTime: 최초 등록 시간
    lastUpdTime: 마지막 수정 시간
    firstRegUser: 최초 등록 사용자
    lastUpdUser: 마지막 수정 사용자
 */

@MappedSuperclass
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public abstract class BaseEntity {
    @Column(nullable = false)
    LocalDateTime firstRegTime;
    @Column(nullable = false)
    LocalDateTime lastUpdTime;
    @Column(length = 15, nullable = false)
    String firstRegUser;
    @Column(length = 15, nullable = false)
    String lastUpdUser;
}

 NormalBoardEntity

package com.portfolio.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;


/**
 * 일반 게시판 Entity
 * date : 2024.07.26
 * author : 김만기
 ====== Annotation description ======
 Entity: JPA Entity로 선언
 SuperBuilder: 상속 받는 값을을 builder로 사용하기위해 필요
 Table: 테이블 명을 클래스명과 별도로 세팅, index 설정
 Id: pk로 사용할 변수 선언
 GeneratedValue: pk값 자동생성 전략을 선택
 Column: 컬럼명, 길이, notNull 등의 설정
 ====== field description ======
 boardId: 게시판 아이디
 title: 게시판 제목
 content: 게시판 내용
 */

@Entity
@Getter
@Setter
@SuperBuilder
@Table(name = "tbl_normal_board", indexes =
        @Index(name = "tbl_normal_board_idx_01", columnList = "first_reg_time")
)
@NoArgsConstructor
@AllArgsConstructor

public class NormalBoard extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String boardId;

    @Column(length = 30, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;
}

 

실행후 JPA로 Table 생성 확인

 


NormalBoardRepo

package com.portfolio.repo;

import com.portfolio.entity.NormalBoard;
import org.springframework.data.jpa.repository.JpaRepository;

/*
    * date: 2024-07-26
    * author: 김만기
    * JpaRepository를 상속받아 NormalBoard 엔티티를 관리하는 레포지토리
 */
public interface NormalBoardRepo extends JpaRepository<NormalBoard, String> {
}

 


CreateNormalBoardDto

package com.portfolio.Dto;

import com.portfolio.entity.NormalBoard;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;


/**
 * 일반 게시판 DTO
 date : 2024.07.26
 author : 김만기

 ====== Annotation description ======
 Schema: swagger 문서 작성을 위한 annotation

 ====== field description ======
 title: 게시판 제목
 content: 게시판 내용

 ====== method description ======
 toEntity: DTO를 DB에 적재하기 위한 Entity로 변환

 ====== etc description ======
 entity를 사용하고 싶었으나 entity를 사용하면 Swagger의 example Value가 모든 필드를 표기하는 문제가 있어서 DTO를 사용

 */
@Getter
@Setter
public class CreateNormalBoardDto {
    @Schema(description = "게시판 제목", example = "게시판 제목")
    private String title;

    @Schema(description = "게시판 내용", example = "게시판 내용")
    private String content;

    public NormalBoard toEntity() {
        return NormalBoard.builder()
                .title(title)
                .content(content)
                .build();
    }
}

 

NormalBoardService

package com.portfolio.service;

import com.portfolio.entity.NormalBoard;

import java.util.List;
/*
    * 일반 게시판 서비스 인터페이스
    * date: 2024-07-26
    * author: 김만기
    ====== method description ======
    * insertNormalBoard: 일반 게시판 등록
 */
public interface NormalBoardService {
    public void insertNormalBoard(NormalBoard board);

    public List<NormalBoard> selectAllBoardList();
}

 

NormalBoardServiceImpl

package com.portfolio.serviceimpl;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
/*
    * 일반 게시판 서비스 구현체
    * date: 2024-07-26
    * author: 김만기
    *
    * ====== Annotation description ======
    * Service: 서비스 빈으로 등록
    *
    * ====== method description ======
    * insertNormalBoard: 일반 게시판 등록
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;

    @Override
    public void insertNormalBoard(NormalBoard board) {
        try {
            boardRepo.save(board);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }
}

 

ApiResultDto

 

 

NormalBoardController

package com.portfolio.controller;

import com.portfolio.Dto.CreateNormalBoardDto;
import com.portfolio.dto.ApiResultDto;
import com.portfolio.entity.NormalBoard;
import com.portfolio.service.NormalBoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

/*
  * 일반 게시판 컨트롤러
  * date: 2024-07-26
  * author: 김만기

  *  ====== Annotation description ======
  * Tag: swagger 문서 작성을 위한 annotation(NormalBoardController를 한 그룹으로 세팅)
  * Operation: swagger 문서 작성을 위한 annotation(메소드별 설명 작성)
 */
@RestController
@RequestMapping("/api/normal-board")
@RequiredArgsConstructor
@Tag(name = "NormalBoard", description = "일반 게시판 관련 API")
public class NormalBoardController {

    private final NormalBoardService normalBoardService;

    @PostMapping("/insert")
    @Operation(summary = "게시물 등록")
    public ResponseEntity<ApiResultDto<NormalBoard>> insertNormalBoard(
            @RequestBody CreateNormalBoardDto board
    ) {
        ApiResultDto<NormalBoard> result = new ApiResultDto<>();
        try {
            NormalBoard normalBoard = board.toEntity();

            normalBoard.setFirstRegTime(LocalDateTime.now());
            normalBoard.setLastUpdTime(LocalDateTime.now());

            // 로그인 기능이 없기 때문이 임시세팅
            normalBoard.setFirstRegUser("admin");
            normalBoard.setLastUpdUser("admin");

            normalBoardService.insertNormalBoard(normalBoard);
            result.setData(normalBoard);
            result.setResultCode("200");
            result.setResultMessage("Success");
        } catch (Exception e) {
            result.setResultCode("500");
            result.setResultMessage("Fail");
        }

        return new ResponseEntity<>(result, result.getResultCode().equals("200") ? org.springframework.http.HttpStatus.OK
                : org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 


TEST

Example Value default 값 확인

 

Insert 쿼리 확인

 

임시 Select 쿼리를 사용한 조회

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

let data = [
  {"id": "ROOT", "name" : "ROOT", "upperId": '', "level" : 0, "order": 0},

  {"id": "HOME",   "name" : "HOME",   "upperId": 'ROOT', "level" : 1, "order": 1},
  {"id": "MOBILE", "name" : "MOBILE", "upperId": 'ROOT', "level" : 1, "order": 2},
  {"id": "COMMON", "name" : "COMMON", "upperId": 'ROOT', "level" : 1, "order": 3},

  {"id": "cH1", "name" : "카테고리1", "upperId": 'HOME', "level" : 2, "order": 1},
  {"id": "cH2", "name" : "카테고리2", "upperId": 'HOME', "level" : 2, "order": 2},
  {"id": "cM3", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 3},
  {"id": "cM4", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 4},
  {"id": "cM5", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 5},

  {"id": "c1s1", "name" : "시나리오1", "upperId": 'cH1', "level" : 3, "order": 2},
  {"id": "c1s2", "name" : "시나리오2", "upperId": 'cH1', "level" : 3, "order": 1},
  {"id": "c1s3", "name" : "시나리오3", "upperId": 'cH1', "level" : 3, "order": 3},
  {"id": "c2s4", "name" : "시나리오4", "upperId": 'cH2', "level" : 3, "order": 4},
  {"id": "c2s5", "name" : "시나리오5", "upperId": 'cH2', "level" : 3, "order": 6},
  {"id": "c2s6", "name" : "시나리오6", "upperId": 'cH2', "level" : 3, "order": 5},
  {"id": "c2s7", "name" : "시나리오7", "upperId": 'cH2', "level" : 3, "order": 7},
  {"id": "c3s8", "name" : "시나리오8", "upperId": 'cM3', "level" : 3, "order": 8},
]

function dataSort(data){
  return data.sort((a, b) => a.level - b.level || a.order - b.order )
}

function makeHierachy(data) {
  // root를 제외한 모든 id 별 포함되는데이터 추출(upperId를 사용)
  const dataMap = data.filter(x => x.level !== 0).reduce((acc, x) => {
    if (!acc[x.upperId]) {
      acc[x.upperId] = []
    }
    // order를 재정의
    x.order = acc[x.upperId].length + 1
    acc[x.upperId].push(x)

    return acc
  }, {})

  // 각 데이터들에 children에 데이터 할당
  for(const d of data) {
    if (dataMap[d.id]) {
      d.children = dataMap[d.id]
    } else {
      delete d.children
    }

  }

}

// 데이터 조회
function look(data) {
  console.log(String(data.name).padStart(data.level * 5)  )
  if(!data.children){
    return
  }
  for(const d of data.children){
    look(d)
  }
}

dataSort(data)
makeHierachy(data)
console.log(data)
// 루트부터 깊이 우선 탐색으로 조회
look(data[0])

 

 

문제

연계기관에서 API를 통해 데이터를 가져가는 케이스가 있다. 

데이터를 가져가게 되면 TRAN_YN을 'Y'로 update 하는데 테스트 요청 시 마다 'N'으로 업데이트를 해주어야한다...

... 나는 언제 퇴근 하라고...

 

해결

MYSQL에 Event기능이 있다고 하여 추가 하였다.

 

이벤트 생성

CREATE EVENT IF NOT EXISTS FRANS_SET
 ON SCHEDULE
   EVERY 1 MINUTE
   STARTS '2024-06-20 15:30:00'
  DO UPDATE TBL_XXXX SET TRANS_YN ='N'

 

이벤트 조회

SHOW EVENTS;

 

이벤트 삭제

DROP EVENT TRAN_SET

 

 

얏호 퇴근하자.

Kafka로 부터 동일한 데이터가 3번 들어오는 케이스가 있었음.

 

어플리케이션의 동작이 정상적이지 않아 로그를 확인해보니 Kafka로부터 동일한 데이터가 3번 들어오는 케이스가 있었다.

해당 데이터를 받아 DB에 insert를 하는 작업인데 동일한 pk로 3개의 데이터가 들어오니 Duplicate PK 관련 에러가 발생하였다.

 

모니터링 툴을 사용하여 확인한 결과 DB 팀에서 DB 변경 Alter Table을 수행한 것이 원인으로 테이블에 대한 lock으로 인해 처리를 하지 못하고 데이터가 리밸런싱 되었다.

+ Recent posts