Java/JPA

JPA 설정 및 기초 - AWS 풀스택 과정 82일차

awspspgh 2024. 11. 21. 09:38
목차
1. 설정
2. 기초

 

1. 설정

 

2. 기초

build.gradle (bootJPA)

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.2.11'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.ezen'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	// https://mvnrepository.com/artifact/nz.net.ultraq.thymeleaf/thymeleaf-layout-dialect
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1
	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
	annotationProcessor 'org.projectlombok:lombok'
	/*providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'*/
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 application.properties

spring.application.name=boot_JPA
server.port=8089

spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/bootdb2
spring.datasource.username=springUser
spring.datasource.password=mysql

spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

spring.thymeleaf.cache=false

 

 log4jdbclog4jdbc.log4j2.properties

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator

 

 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %-5p [%c] %msg%n</pattern>
        </encoder>
    </appender>
    <logger name="com.ezen.boot_JPA" level="INFO" appender-ref="STDOUT"/>
    <logger name="jdbc" level="OFF"/>
    <logger name="jdbc.connection" level="OFF"/>
    <logger name="jdbc.audit" level="OFF"/>
    <logger name="jdbc.sqlonly" level="INFO" appender-ref="STDOUT"/>
    <logger name="jdbc.resultsettable" level="INFO" appender-ref="STDOUT"/>
    <logger name="org.springframework" level="error"/>
    <logger name="org.springframework.jdbc" level="error"/>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

 

 BootJpaApplication.java

package com.ezen.boot_JPA;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class BootJpaApplication {

	public static void main(String[] args) {
		SpringApplication.run(BootJpaApplication.class, args);
	}

}

 

 layout.html

<!DOCTYPE html>
<html lang="en"
  xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <meta charset="UTF-8">
    <title>boot_JPA</title>
  <!--/* 부트스트랩 연결 링크  css 연결링크 */-->
    <link rel="stylesheet" th:href="@{/dist/css/bootstrap.min.css}">
    <script th:src="@{/dist/js/bootstrap.bundle.min.js}"></script>
</head>
<body>
    <!--/* th:replace="~{조각 파일의 경로 :: 이름}" */-->
    <div th:replace="~{fragments/header :: header}"></div>

    <div layout:fragment="content"></div>

    <div th:replace="~{fragments/footer :: footer}"></div>
</body>
</html>

 

 header.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--/* th:fragment="이름" : 레이아웃에서 사용할 조각 */-->
<div th:fragment="header">
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">Boot</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="/">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/board/register">BoardRegister</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/board/list">BoardList</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</div>

 

 footer.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--/* th:fragment="이름" : 레이아웃에서 사용할 조각 */-->
<div th:fragment="footer">
    <h5>Footer Area</h5>
</div>

 

 index.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container-md">
        <h1>My Spring Boot_JPA Project</h1>
    </div>
</div>

 

 Board.java

package com.ezen.boot_JPA.entity;

import jakarta.persistence.*;
import lombok.*;

/* Entity : DB의 테이블 클래스
*  DTO : 객체를 생성하는 클래스
*  자주쓰는 코드들 : base class로 별도 관리
* regDate / modDate
*
* id  = 기본키
* 기본키 생성 전략 : GeneratedValue
* */
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends TimeBase{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increments 생성
    private Long bno;

    @Column(length = 200, nullable = false)
    private String title;
    @Column(length = 200, nullable = false)
    private String writer;
    @Column(length = 2000, nullable = false)
    private String content;
    
    // 생성시 초기화 값을 지정 : 객체가 생길 때 객체의 기본값 생성
    //@Builder.Default
    //private int point = 0;
}

 

 TimeBase.java

package com.ezen.boot_JPA.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class}) // 반드시 지정
@Getter
public class TimeBase {
    /* 등록일, 수정일만 따로 빼서 관리하는 슈퍼 테이블 */
    @CreatedDate
    @Column(name = "reg_at", updatable = false)
    private LocalDateTime regAt;

    @LastModifiedDate
    @Column(name = "mod_at")
    private LocalDateTime modAt;
}

 

BoardDTO.java

package com.ezen.boot_JPA.dto;

import lombok.*;

import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BoardDTO {
    private Long bno;
    private String title;
    private String writer;
    private String content;
    private LocalDateTime regAt, modAt;

}

 

 BoardController.java

package com.ezen.boot_JPA.controller;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@Slf4j
@RequestMapping("/board/*")
@RequiredArgsConstructor
@Controller
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/register")
    public void register(){}

    @PostMapping("/register")
    public String register(BoardDTO boardDTO){
        log.info(">>> boardDTO >>> {}", boardDTO);
        // insert, update, delete => return 1 row
        // jpa insert, update, delete => return id
        Long bno = boardService.insert(boardDTO);
        log.info(">>> insert >>> {}", bno > 0 ? "OK" : "FAIL");
        return "/index";
    }

    @GetMapping("/list")
    public void list(Model model){
        List<BoardDTO> list = boardService.getList();
        model.addAttribute("list", list);
    }

    @GetMapping("/detail")
    public void modify(Model model, @RequestParam("bno") Long bno){
        BoardDTO boardDTO = boardService.getDetail(bno);
        model.addAttribute("boardDTO", boardDTO);
    }

    @PostMapping("/modify")
    public String modify(BoardDTO boardDTO, RedirectAttributes redirectAttributes){
        Long bno = boardService.modify(boardDTO);
        redirectAttributes.addAttribute("bno", boardDTO.getBno());
        return "redirect:/board/detail";
    }

    @GetMapping("/delete")
    public String delete(@RequestParam("bno") Long bno){
        boardService.delete(bno);
        return "redirect:/board/list";
    }
}

 

 BoardService.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.entity.Board;

import java.util.List;

public interface BoardService {
    // 추상 메서드만 가능한 인터페이스
    // 메서드가 default(접근제한자) 구현 가능
    Long insert(BoardDTO boardDTO);

    // BoardDTO(class): bno title writer content regAt modAt
    // Board(table) : bno title writer content
    // BoardDTO => board 변환
    // 화면에서 가져온 BoardDTO 객체를 저장을 위한 Board 객체로 변환
    default Board convertDtoToEntity(BoardDTO boardDTO){
        return Board.builder()
                .bno(boardDTO.getBno())
                .title(boardDTO.getTitle())
                .writer(boardDTO.getWriter())
                .content(boardDTO.getContent())
                .build();
    }

    // board => BoardDTO 변환
    // DB에서 가져온 Board 객체를 화면에 뿌리기 위한 BoardDTO 객체로 변환
    default BoardDTO convertEntityToDto(Board board){
        return BoardDTO.builder()
                .bno(board.getBno())
                .title(board.getTitle())
                .writer(board.getWriter())
                .content(board.getContent())
                .regAt(board.getRegAt())
                .modAt(board.getModAt())
                .build();
    }

    List<BoardDTO> getList();

    BoardDTO getDetail(Long bno);

    Long modify(BoardDTO boardDTO);

    void delete(Long bno);
}

 

 BoardServiceImpl.java

package com.ezen.boot_JPA.service;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.entity.Board;
import com.ezen.boot_JPA.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService{
    private final BoardRepository boardRepository;

    @Override
    public Long insert(BoardDTO boardDTO) {
        // 저장 객체는 Board
        // save() : insert 후 저장 객체의 id를 리턴
        // save() Entity 객체를 파라미터로 전송
        return boardRepository.save(convertDtoToEntity(boardDTO)).getBno();
    }

    @Override
    public List<BoardDTO> getList() {
        // 컨트롤러로 보내야 하는 리턴은 List<BoardDTO>
        // DB에서 가져오는 리턴은 List<Board> > BoardDTO 객체로 변환
        // findAll()
        // 정렬 : Sort.by(Sort.Direction.DESC, "정렬기준 칼럼명")
        List<Board> boardList = boardRepository.findAll(Sort.by(Sort.Direction.DESC, "bno"));
/*        List<BoardDTO> boardDTOList = new ArrayList<>();
        for(Board board : boardList){
            boardDTOList.add(convertEntityToDto(board));
        }*/
        List<BoardDTO> boardDTOList = boardList.stream()
                .map(b -> convertEntityToDto(b)).toList();
        return boardDTOList;
    }

    @Override
    public BoardDTO getDetail(Long bno) {
        /* findById : 아이디(PK)를 주고 해당 객체를 리턴
            findById의 리턴타입 Optional<Board> 타입으로 리턴
            Optional<T> : nullPointException이 발생하지 않도록 도와줌.
            Optional.isEmpty() : null일 경우 확인 가능 true / false
            Optional.isPresent() : 값이 있는지 확인 true / false
            Optional.get() : 객체 가져오기
        * */
        Optional<Board> optional = boardRepository.findById(bno);
        if(optional.isPresent()){
            BoardDTO boardDTO = convertEntityToDto(optional.get());
            return boardDTO;
        }
        return null;
    }

    @Override
    public Long modify(BoardDTO boardDTO) {
        // update : jpa는 업데이트가 없음.
        // 기존 객체를 가져와서 set 수정 후 다시 저장
        Optional<Board> optional = boardRepository.findById(boardDTO.getBno());
        if(optional.isPresent()){
            Board entity = optional.get();
            entity.setTitle(boardDTO.getTitle());
            entity.setContent(boardDTO.getContent());
            // 다시 저장 (기존 객체에 덮어쓰기)
            return boardRepository.save(entity).getBno();
        }
        return null;
    }

    // 삭제 : deleteById(id)

    @Override
    public void delete(Long bno) {
        boardRepository.deleteById(bno);
    }

}

 

 BoardRepository.java

package com.ezen.boot_JPA.repository;

import com.ezen.boot_JPA.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

/* JpaRepository<테이블, id> */
public interface BoardRepository extends JpaRepository<Board, Long> {
}

 

 BootJpaApplicationTests.java

package com.ezen.boot_JPA;

import com.ezen.boot_JPA.dto.BoardDTO;
import com.ezen.boot_JPA.service.BoardService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = BootJpaApplication.class)
class BootJpaApplicationTests {

	@Autowired
	private BoardService boardService;

	@Test
	void contextLoads() {
		for(int i=0; i<300; i++){
			BoardDTO boardDTO = BoardDTO.builder()
					.title("test title " + i)
					.writer("tester " + ((int)(Math.random()*50)+1))
					.content("Test Content " + i)
					.build();

			boardService.insert(boardDTO);
		}
	}

}

 

 list.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container-md">
        <h1>Board List Page</h1>
        <table class="table table-hover">
            <thead>
            <tr>
                <th>#</th>
                <th>title</th>
                <th>writer</th>
                <th>regDate</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="bvo:${list}">
                <td>[[${bvo.bno}]]</td>
                <td><a th:href="@{/board/detail(bno=${bvo.bno})}">[[${bvo.title}]]</a></td>
                <td>[[${bvo.writer}]]</td>
                <td>[[${bvo.regAt}]]</td>
            </tr>
            </tbody>
        </table>
    </div>
</div>

 

 detail.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout}">

<div layout:fragment="content">
    <div class="container-md">
        <h1>Board Detail Page [[boardDTO.bno]]</h1>

        <form action="/board/modify" method="post" id="modForm">
            <div class="mb-3">
                <label for="r" class="form-label">Created At</label>
                <input type="text" class="form-control" name="regDate" id="r" th:value="${boardDTO.regAt}" readonly>
            </div><div class="mb-3">
            <input type="hidden" class="form-control" name="bno" id="n" th:value="${boardDTO.bno}">
        </div>
            <div class="mb-3">
                <label for="title" class="form-label">Title</label>
                <input type="text" class="form-control" name="title" id="title" th:value="${boardDTO.title}" readonly>
            </div>
            <div class="mb-3">
                <label for="writer" class="form-label">Writer</label>
                <input type="text" class="form-control" name="writer" id="writer" th:value="${boardDTO.writer}" readonly>
            </div>
            <div class="mb-3">
                <label for="content" class="form-label">Content</label>
                <textarea class="form-control" name="content" id="content" rows="3" readonly>[[${boardDTO.content}]]</textarea>
            </div>
            <button type="button" id="listBtn" class="btn btn-primary">List</button>
            <button type="button" id="modBtn" class="btn btn-warning">Modify</button>
            <a th:href="@{/board/delete(bno=${boardDTO.bno})}">
                <button type="button" id="delBtn" class="btn btn-danger">Delete</button>
            </a>
        </form>
    </div>
    <script th:src="@{/js/boardDetail.js}"></script>
</div>

 

boardDetail.js

console.log("board detail.js in!!");

document.getElementById('listBtn').addEventListener('click', ()=>{
    location.href = "/board/list";
});

document.getElementById('modBtn').addEventListener('click',()=>{
    document.getElementById('title').readOnly=false;
    document.getElementById('content').readOnly=false;

    // 버튼 생성
    let modBtn = document.createElement("button");
    modBtn.setAttribute("type","submit");
    modBtn.classList.add("btn","btn-outline-warning");
    modBtn.innerText="Submit";

    // 추가
    document.getElementById("modForm").appendChild(modBtn);
    document.getElementById("modBtn").remove();
    document.getElementById("delBtn").remove;
});

 

출력