코딩/Spring Boot

Spring Boot JPA를 사용한 PostgreSQL 연동

skyoon 2025. 1. 15. 09:08

Git Code 버전 : 0.0.3-SNAPSHOT

  Spring Boot에서 JPA를 사용하기 위해 build.gradle의 dependencies 안에 아래와 같이 라이브러리를 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'

  DB 연결 설정을 applicaiton.yml 파일에 아래와 같이 설정해 준다. 크게 2 부분으로 jpa를 사용하기 위한 설정과 jpa를 통해 연결할 PostgreSQL 연결 설정이다. 중간 ddl-auto 옵션만 주의해서 설정하는데 JPA의 좋은 점으로 DB에 접속해서 Table이 없으면 Table을 생성해 주고 Table이 생성되어 있으면 그대로 둔다.

spring:
  jpa:
    hibernate:
      ddl-auto: update	# DB에 Table과 Data가 존재하면 생성하지 않고, Table이 없으면 생성
    show-sql: true
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    open-in-view: false
    generate-ddl: false

  datasource:
    url: jdbc:postgresql://${POSTGRESQL_HOST}:5432/stock
    username: ${POSTGRESQL_USER}
    password: ${POSTGRESQL_PASSWORD}
    driver-class-name: org.postgresql.Driver

  실제 Web Application을 서비스하기 위해서는 DB 서버의 주소, 아이디, 비밀번호를 Git에 올릴 수 없으므로 아래와 같이 설정해 주고 bashrc 또는 zshrc 같은 환경 설정파일에 export 명령을 사용해서 환경변수 설정을 해주면 Git으로는 코드만 관리하고 설정은 따로 관리할 수 있다.

export POSTGRESQL_HOST=localhost
export POSTGRESQL_USER=sky
export POSTGRESQL_PASSWORD=123456

  PostgreSQL과 연결은 이것으로 끝난다. 이제 DB와 데이터를 CURD 하기 위한 JPA 코드를 작성하기 위해 Entity를 하나 생성한다. 생성전 각각의 역할에 맞는 소스 코드를 아래와 같은 구조로 생성한다.

  • Data가 저장이 되는 entity 폴더
  • Entity 제어를 위한 Repository Interface가 있는 repo 폴더
  • control에서 Repository를 사용할 수 있도록 service 폴더
    • 개인적으로는 service 폴더에는 interface만 담기 위해 하위에 직접 구현한 class를 저장할 impl 폴더를 추가로 생성한다.

  entity 폴더 아래 Config.java(data.go.kr 접속 정보를 저장할 table) 파일을 생성해 주는데 Entity는 DB의 Table에 연동하는 Class로 Class명이 DB Table명이 된다. 그러므로 실제 DB에 접속하면 config 테이블이 생성된 것을 확인 할 수 있다.

  • Config.java
더보기
package io.personal.stock.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
public class Config {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    String confName; // 설정 변수 Key name

    String confValue; // 설정 변수 Value

    String category; // 설정 변수 카테고리

    String comment; // 설정 변수 설명
}

  실제 DB에 CURD 동작을 수행하는 JpaRepostory를 상속받는 Interface를 repo아래 생성해준다. (기본 CURD는 JPA에서 제공해주고 개발자는 필요한 기능만 구현하면 됨)

  • ConfigRepository.java
더보기
package io.personal.stock.repo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import io.personal.stock.entity.Config;

@Repository
public interface ConfigRepository extends JpaRepository<Config, Long> {

}

  JPA에서 제공하는 기능 및 추가 Method등의 Service들을 구현할 interface와 class를 생성해준다. interface는 service 폴더 아래, class는 imple 폴더 아래 만들어준다.

  • ConfigService.java
더보기
package io.personal.stock.service;

import java.util.List;

import io.personal.stock.entity.Config;

public interface ConfigService {
    public List<Config> getAllConfig();

    public Config save(Config item);

    public void delete(Long id);
}
  • ConfigServiceImpl.java
더보기
package io.personal.stock.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import io.personal.stock.entity.Config;
import io.personal.stock.repo.ConfigRepository;

public class ConfigServcieImpl {

    @Autowired
    ConfigRepository configRepo;

    public List<Config> getAllConfig() {
        return configRepo.findAll();
    }

    @Transactional
    public Config save(Config item) {
        return configRepo.save(item);
    }

    @Transactional
    public void delete(Long id) {
        configRepo.deleteById(id);
    }
}

 

  동작 확인을 위해 UI 페이지를 생성한다. ConfigController를 controlle에 추가해준다.

더보기
package io.personal.stock.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import io.personal.stock.entity.Config;
import io.personal.stock.service.ConfigService;

@Controller
public class ConfigController {

    @Autowired
    ConfigService configService;

    @GetMapping(value = "/conf")
    public String conf(Model model) {

        List<Config> configInfos = configService.getAllConfig();
        model.addAttribute("confs", configInfos);

        return "conf";
    }

    @PostMapping("/conf/add")
    @ResponseBody
    public ResponseEntity<String> addNewConfig(@RequestBody Config config) {
        configService.save(config);

        return ResponseEntity.ok("Config saved successfully");
    }

    @DeleteMapping("/conf/delete/{id}")
    public ResponseEntity<String> deleteConfig(@PathVariable Long id) {
        configService.delete(id);

        return ResponseEntity.ok("Config deleted successfully"); // 성공 메시지를 JSON 형태로 반환
    }
}

 

  resource/templates아래 conf.html을 생성해준다.

  • config.html
더보기
<!DOCTYPE html>
<html lang="ko">
    <head th:replace="~{layouts/header :: headerFragment(~{::title})}">
        <title>Config</title>
    </head>
    <body class="sb-nav-fixed">
        <nav th:replace="~{layouts/top :: top-nav}"></nav>
        <div id="layoutSidenav">
            <!-- Left Side Menu-->
            <div th:replace="~{layouts/left-side :: side-nav}"></div>

            <div id="layoutSidenav_content">
                <main>
                    <div class="container-fluid px-4">
                        <h1 class="mt-4">사이트 설정</h1>
                        <ol class="breadcrumb mb-4">
                            <li class="breadcrumb-item active">사이트 설정</li>
                        </ol>
                        <div class="row">
                            <div class="md-12">
                                <div class="card-head">
                                    <!-- Button to trigger modal -->
                                    <button
                                        type="button"
                                        class="btn btn-sm btn-primary"
                                        data-bs-toggle="modal"
                                        data-bs-target="#configModal">
                                        Add Config
                                    </button>
                                </div>
                            </div>
                        </div>

                        <div class="row mt-5">
                            <div class="md-12">
                                <div class="card-body">
                                    <table id="itemTable" class="datatable-table">
                                        <thead>
                                            <tr>
                                                <th>ID</th>
                                                <th>Key</th>
                                                <th>Value</th>
                                                <th>Category</th>
                                                <th>Comment</th>
                                                <th>Delete</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr th:each="conf: ${confs}">
                                                <td th:text="${conf.id}">ID</td>
                                                <td th:text="${conf.confName}">Key</td>
                                                <td th:text="${conf.confValue}">Value</td>
                                                <td th:text="${conf.category}">Category</td>
                                                <td th:text="${conf.comment}">Comment</td>
                                                <td>
                                                    <button
                                                        class="btn btn-danger btn-sm delete-btn"
                                                        th:data-id="${conf.id}">
                                                        Delete
                                                    </button>
                                                </td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Modal -->
                    <div
                        class="modal fade"
                        id="configModal"
                        tabindex="-1"
                        aria-labelledby="configModalLabel"
                        aria-hidden="true">
                        <div class="modal-dialog">
                            <div class="modal-content">
                                <div class="modal-header">
                                    <h5 class="modal-title" id="configModalLabel">Add Config</h5>
                                    <button
                                        type="button"
                                        class="btn-close"
                                        data-bs-dismiss="modal"
                                        aria-label="Close"></button>
                                </div>
                                <div class="modal-body">
                                    <form method="post">
                                        <div class="mb-3">
                                            <label for="confName" class="form-label">Name</label>
                                            <input type="text" class="form-control" id="confName" required />
                                        </div>
                                        <div class="mb-3">
                                            <label for="confValue" class="form-label">Value</label>
                                            <input type="text" class="form-control" id="confValue" required />
                                        </div>
                                        <div class="mb-3">
                                            <label for="category" class="form-label">Category</label>
                                            <input type="text" class="form-control" id="category" required />
                                        </div>
                                        <div class="mb-3">
                                            <label for="comment" class="form-label">Comment</label>
                                            <input type="text" class="form-control" id="comment" required />
                                        </div>
                                    </form>
                                </div>
                                <div class="modal-footer">
                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                                        Close
                                    </button>
                                    <button type="button" class="btn btn-primary" id="saveConfig">Save Config</button>
                                </div>
                            </div>
                        </div>
                    </div>
                </main>
                <footer th:replace="~{layouts/footer :: footerFragment}"></footer>
            </div>
        </div>

        <th:block th:replace="~{layouts/scripts :: scriptFragment}"></th:block>
        <script type="text/javascript">
            $(document).ready(function () {
                var table = $("#itemTable").DataTable({
                    destroy: true, // 테이블을 재초기화할 수 있도록 설정
                    searching: true, // 검색 기능 비활성화
                    paging: true, // 페이지네이션 활성화
                    ordering: true, // 정렬 활성화
                });

                $("#saveConfig").click(function () {
                    saveConfig();
                });

                $("#itemTable").on("click", ".delete-btn", function () {
                    var configId = $(this).data("id");
                    if (confirm("Are you sure you want to delete this config?")) {
                        deleteConfig(configId, $(this).closest("tr"));
                    }
                });
            });

            function saveConfig() {
                const configData = {
                    confName: $("#confName").val(),
                    confValue: $("#confValue").val(),
                    category: $("#category").val(),
                    comment: $("#comment").val(),
                };

                $.ajax({
                    type: "POST",
                    url: "/conf/add",
                    contentType: "application/json",
                    data: JSON.stringify(configData),
                    success: function (response) {
                        alert(response); // Display success message
                        $("#configModal").modal("hide"); // Close modal
                        location.reload();
                    },
                    error: function (error) {
                        alert("Error saving config: " + error.responseText);
                    },
                });
            }

            function deleteConfig(id, row) {
                $.ajax({
                    url: "/conf/delete/" + id,
                    type: "DELETE",
                    success: function (response) {
                        alert(response);
                        row.remove(); // Remove row from DataTable
                    },
                    error: function (error) {
                        alert("Error deleting config: " + error.responseText);
                    },
                });
            }
        </script>
    </body>
</html>

  

  정상적이면 터미널에 아래와 같이 로그가 출력된다.

2025-01-15T08:58:48.991+09:00  INFO 94081 --- [stock] [           main] org.hibernate.orm.connections.pooling    : HHH10001005: Database info:
        Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
        Database driver: undefined/unknown
        Database version: 17.2
        Autocommit mode: undefined/unknown
        Isolation level: undefined/unknown
        Minimum pool size: undefined/unknown
        Maximum pool size: undefined/unknown

 

  UI Page에 Config를 클릭하면하위 Config가 있다 클릭해서 동작 확인 Add Config를 누르면 나오는 모달에 데이터를 입력하고 Save Config를 클릭하여면 팝업이 뜨고 결과를 확인하면 정상 등록됨을 확인할 수 있다.

 

  Delete 버튼을 클릭해서 삭제도 확인