[LG CNS AM CAMP 1기] 백엔드 II 7 | SpringBoot

letthem·2025년 2월 4일
0

LG CNS AM CAMP 1기

목록 보기
23/31
post-thumbnail

JPA를 사용하는 게시판으로 변경

자바 애플리케이션에서 관계형 데이터베이스와 객체 간의 매핑을 제공하는 표준 API
JPA는 자바 객체를 데이터베이스 테이블에 매핑하고, 자바 객체와 데이터베이스 간의 데이터 전송을 관리
JPA를 사용하면 데이터베이스 작업을 쉽게 처리할 수 있으며, SQL 코드를 줄이고 객체 지향적인 방식으로 데이터를 다룰 수 있음

  • MSA 환경에서 서비스 별로 디비 분리 -> 디비 연관관계 단순화

의존성 확인

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

⭐️⭐️⭐️ 엔티티 생성 (table 만들기)

데이터베이스 테이블에 매핑되는 자바 클래스
@Entity 애노테이션으로 정의

  • @Data : Getter, Setter 들어있음

BoardEntity

package board.entity;

import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "t_jpa_board")
@Data
@DynamicUpdate // 변경된 필드만 업데이트
public class BoardEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) // 자동 증가. AUTO or IDENTITY
    private int boardIdx;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String contents;

    @Column(nullable = false)
    private int hitCnt;

    @CreationTimestamp
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdDt;

    @Column(nullable = false, updatable = false)
    private String createdId;

    @UpdateTimestamp
    private LocalDateTime updatorDt;

    private String updatorId;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) // (1 : n) / EAGER : 조인. (함께 조회) / Lazy는 지연 로딩.
    @JoinColumn(name = "board_idx")
    private List<BoardFileEntity> fileInfoList;
}

BoardFileEntity

package board.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.DynamicUpdate;

@Entity
@Data
@Table(name = "t_jpa_file")
@DynamicUpdate
public class BoardFileEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int idx;

    // private int boardIdx;

    @Column(nullable = false)
    private String originalFileName;

    @Column(nullable = false)
    private String storedFilePath;

    @Column(nullable = false)
    private String fileSize;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_idx") // 외래키
    private BoardEntity board;
}

⭐️⭐️⭐️ 레포지토리 생성 (DB와 연결 CRUD)

package board.repository;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import board.entity.BoardEntity;
import board.entity.BoardFileEntity;

@Repository
public interface JpaBoardRepository extends CrudRepository<BoardEntity, Integer> {
    // 쿼리 메서드 
    List<BoardEntity> findAllByOrderByBoardIdxDesc();
    
    // @Query 어노테이션으로 실행할 쿼리를 직접 정의
    @Query("SELECT file FROM BoardFileEntity file WHERE file.board.boardIdx = :boardIdx AND file.idx = :idx")
    BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);    
}

⭐️⭐️⭐️ service 생성

Dto 대신 Entity 사용
JpaBoardService

package board.service;

import java.util.List;

import org.springframework.web.multipart.MultipartHttpServletRequest;

import board.entity.BoardEntity;
import board.entity.BoardFileEntity;

public interface JpaBoardService {
    List<BoardEntity> selectBoardList();
    BoardEntity selectBoardDetail(int boardIdx);
    void insertBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception;
    void updateBoard(BoardEntity boardEntity);
    // void saveBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception;
    void deleteBoard(int boardIdx);
    BoardFileEntity selectBoardFileInfo(int idx, int boardIdx);
}

JpaBoardServiceImpl

package board.service;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import board.common.FileUtils;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.repository.JpaBoardRepository;
@Service
public class JpaBoardServiceImpl implements JpaBoardService {
    @Autowired
    private JpaBoardRepository jpaBoardRepository;

    @Autowired
    private FileUtils fileUtils;

    @Override
    public List<BoardEntity> selectBoardList() {
        return jpaBoardRepository.findAllByOrderByBoardIdxDesc();
    }

    @Override
    public BoardEntity selectBoardDetail(int boardIdx) {
        Optional<BoardEntity> optional = jpaBoardRepository.findById(boardIdx);
        if (optional.isPresent()) {
            BoardEntity board = optional.get();

            board.setHitCnt(board.getHitCnt() + 1);
            jpaBoardRepository.save(board);

            return board;
        } else {
            throw new RuntimeException();
        }
    }

    /*
    @Override
    public void saveBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception {
        boardEntity.setCreatedId("admin");
        BoardEntity existingBoard = jpaBoardRepository.findById(boardEntity.getBoardIdx()).orElse(null);
        if (existingBoard != null) {
            // boardEntity.setFileInfoList(existingBoard.getFileInfoList());
        } else {
            List<BoardFileEntity> list = fileUtils.parseFileInfo2(boardEntity.getBoardIdx(), request);
            if (!CollectionUtils.isEmpty(list)) {
                boardEntity.setFileInfoList(list);
            }
        }
        jpaBoardRepository.save(boardEntity);
    }
    */

    @Override
    public void insertBoard(BoardEntity boardEntity, MultipartHttpServletRequest request) throws Exception {
        boardEntity.setCreatedId("admin");
        List<BoardFileEntity> list = fileUtils.parseFileInfo2(boardEntity.getBoardIdx(), request);
        if (!CollectionUtils.isEmpty(list)) {
            boardEntity.setFileInfoList(list);
        }
        jpaBoardRepository.save(boardEntity);
    }

    @Override
    public void updateBoard(BoardEntity boardEntity) {
        BoardEntity existingBoard = jpaBoardRepository.findById(boardEntity.getBoardIdx()).orElse(null);
        if (existingBoard != null) {
            boardEntity.setFileInfoList(existingBoard.getFileInfoList());
        }
        jpaBoardRepository.save(boardEntity);
    }


    @Override
    public void deleteBoard(int boardIdx) {
        jpaBoardRepository.deleteById(boardIdx);
    }

    @Override
    public BoardFileEntity selectBoardFileInfo(int idx, int boardIdx) {
        return jpaBoardRepository.findBoardFile(boardIdx, boardIdx);
    }
}

FileUtils

package board.common;

import java.io.File;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import board.entity.BoardFileEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import board.dto.BoardFileDto;

@Component
public class FileUtils {
    @Value("${spring.servlet.multipart.location}")
    private String uploadDir;

    // 요청을 통해서 전달받은 파일을 저장하고, 파일 정보를 반환하는 메서드
    public List<BoardFileDto> parseFileInfo(int boardIdx, MultipartHttpServletRequest request) throws Exception {
        if (ObjectUtils.isEmpty(request)) {
            return null;
        }

        List<BoardFileDto> fileInfoList = new ArrayList<>();

        // 파일을 저장할 디렉터리를 설정
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
        ZonedDateTime now = ZonedDateTime.now();
        String storedDir = uploadDir + "images\\" + now.format(dtf);
        File fileDir = new File(storedDir);
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        }

        // 업로드 파일 데이터를 디렉터리에 저장하고 정보를 리스트에 저장
        Iterator<String> fileTagNames = request.getFileNames();
        while(fileTagNames.hasNext()) {
            String fileTagName = fileTagNames.next();
            List<MultipartFile> files = request.getFiles(fileTagName);
            for (MultipartFile file : files) {
                String originalFileExtension = "";

                // 파일 확장자를 ContentType에 맞춰서 지정
                if (!file.isEmpty()) {
                    String contentType = file.getContentType();
                    if (ObjectUtils.isEmpty(contentType)) {
                        break;
                    } else {
                        if (contentType.contains("image/jpeg")) {
                            originalFileExtension = ".jpg";
                        } else if (contentType.contains("image/png")) {
                            originalFileExtension = ".png";
                        } else if (contentType.contains("image/gif")) {
                            originalFileExtension = ".gif";
                        } else {
                            break;
                        }
                    }

                    // 저장에 사용할 파일 이름을 조합
                    String storedFileName = Long.toString(System.nanoTime()) + originalFileExtension;
                    String storedFilePath = storedDir + "\\" + storedFileName;

                    // 파일 정보를 리스트에 저장
                    BoardFileDto dto = new BoardFileDto();
                    dto.setBoardIdx(boardIdx);
                    dto.setFileSize(Long.toString(file.getSize()));
                    dto.setOriginalFileName(file.getOriginalFilename());
                    dto.setStoredFilePath(storedFilePath);
                    fileInfoList.add(dto);

                    // 파일 저장
                    fileDir = new File(storedFilePath);
                    file.transferTo(fileDir);
                }
            }
        }

        return fileInfoList;
    }

    public List<BoardFileEntity> parseFileInfo2(int boardIdx, MultipartHttpServletRequest request) throws Exception {
        if (ObjectUtils.isEmpty(request)) {
            return null;
        }

        List<BoardFileEntity> fileInfoList = new ArrayList<>();

        // 파일을 저장할 디렉터리를 설정
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
        ZonedDateTime now = ZonedDateTime.now();
        String storedDir = uploadDir + "images\\" + now.format(dtf);
        File fileDir = new File(storedDir);
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        }

        // 업로드 파일 데이터를 디렉터리에 저장하고 정보를 리스트에 저장
        Iterator<String> fileTagNames = request.getFileNames();
        while(fileTagNames.hasNext()) {
            String fileTagName = fileTagNames.next();
            List<MultipartFile> files = request.getFiles(fileTagName);
            for (MultipartFile file : files) {
                String originalFileExtension = "";

                // 파일 확장자를 ContentType에 맞춰서 지정
                if (!file.isEmpty()) {
                    String contentType = file.getContentType();
                    if (ObjectUtils.isEmpty(contentType)) {
                        break;
                    } else {
                        if (contentType.contains("image/jpeg")) {
                            originalFileExtension = ".jpg";
                        } else if (contentType.contains("image/png")) {
                            originalFileExtension = ".png";
                        } else if (contentType.contains("image/gif")) {
                            originalFileExtension = ".gif";
                        } else {
                            break;
                        }
                    }

                    // 저장에 사용할 파일 이름을 조합
                    String storedFileName = Long.toString(System.nanoTime()) + originalFileExtension;
                    String storedFilePath = storedDir + "\\" + storedFileName;

                    // 파일 정보를 리스트에 저장
                    BoardFileEntity entity = new BoardFileEntity();
                    /*
                    entity.setBoardIdx(boardIdx);
                    */
                    entity.setFileSize(String.valueOf(file.getSize()));
                    entity.setOriginalFileName(file.getOriginalFilename());
                    entity.setStoredFilePath(storedFilePath);
                    fileInfoList.add(entity);

                    // 파일 저장
                    fileDir = new File(storedFilePath);
                    file.transferTo(fileDir);
                }
            }
        }

        return fileInfoList;
    }

}

컨트롤러 생성

JpaBoardController

package board.controller;

import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;

import board.dto.BoardInsertRequest;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.service.JpaBoardService;
import jakarta.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/jpa")
public class JpaBoardController {
    @Autowired
    private JpaBoardService boardService;
    
    // 목록 조회
    @GetMapping("/board")
    public ModelAndView openBoardList() throws Exception {
        ModelAndView mv = new ModelAndView("/board/jpaBoardList");
        
        List<BoardEntity> list = boardService.selectBoardList();
        mv.addObject("list", list);
        
        return mv;
    }
    
    // 작성 화면
    @GetMapping("/board/write")
    public String openBoardWrite() throws Exception {
        return "/board/jpaBoardWrite";
    }
    
    // 저장 처리
    @PostMapping("/board/write")
    public String insertBoard(BoardInsertRequest boardInsertRequest, MultipartHttpServletRequest request) throws Exception {
        BoardEntity boardEntity = new ModelMapper().map(boardInsertRequest, BoardEntity.class);
        
        boardService.insertBoard(boardEntity, request);
        return "redirect:/jpa/board";
    }
    
    // 상세 조회
    @GetMapping("/board/{boardIdx}")
    public ModelAndView openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
        BoardEntity boardEntity = boardService.selectBoardDetail(boardIdx);
        ModelAndView mv = new ModelAndView("/board/jpaBoardDetail");
        mv.addObject("board", boardEntity);
        return mv;
    }
    
    // 수정 처리
    @PutMapping("/board/{boardIdx}")
    public String updateBoard(@PathVariable("boardIdx") int boardIdx, BoardEntity boardEntity) throws Exception {
        boardEntity.setBoardIdx(boardIdx);
        boardService.updateBoard(boardEntity);
        return "redirect:/jpa/board";
    }
    
    // 삭제 처리
    @DeleteMapping("/board/{boardIdx}")
    public String deleteBoard(@RequestParam("boardIdx") int boardIdx) throws Exception {
        boardService.deleteBoard(boardIdx);
        return "redirect:/jpa/board";
    }
    
    // 첨부파일 다운로드 
    @GetMapping("/board/file")
    public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
        BoardFileEntity boardFileEntity = boardService.selectBoardFileInfo(idx, boardIdx);
        if (ObjectUtils.isEmpty(boardFileEntity)) {
            return;
        }
        
        Path path = Paths.get(boardFileEntity.getStoredFilePath());
        byte[] file = Files.readAllBytes(path);
        
        response.setContentType("application/octet-stream");
        response.setContentLength(file.length);
        response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(boardFileEntity.getOriginalFileName(), "UTF-8") + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.getOutputStream().write(file);
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }
}

jpaBoardDetail.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JPA 게시판</title>
<link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
	<div class="container">
		<h2>JPA 게시판 상세</h2>
		<form id="frm" method="post">
			<input type="hidden" id="boardIdx" name="boardIdx"
				th:value="${board.boardIdx}" />

			<!-- HiddenHttpMethodFilter 에서 요청 방식 결정에 사용할 값을 전달하는 용도 -->
			<input type="hidden" id="method" name="_method" />

			<table class="board_detail">
				<colgroup>
					<col width="15%" />
					<col width="*" />
					<col width="15%" />
					<col width="35%" />
				</colgroup>
				<tr>
					<th>글번호</th>
					<td th:text="${board.boardIdx}"></td>
					<th>조회수</th>
					<td th:text="${board.hitCnt}"></td>
				</tr>
				<tr>
					<th>작성자</th>
					<td th:text="${board.createdId}"></td>
					<th>작성일</th>
					<td th:text="${board.createdDt}"></td>
				</tr>
				<tr>
					<th>제목</th>
					<td colspan="3"><input type="text" id="title" name="title"
						th:value="${board.title}" /></td>
				</tr>
				<tr>
					<td colspan="4"><textarea id="contents" name="contents"
							th:text="${board.contents}"></textarea></td>
				</tr>
			</table>
		</form>

		<!-- 첨부파일 목록 -->
		<div class="file_list">
			<a th:each="file : ${board.fileInfoList}"
				th:text="|${file.originalFileName} (${file.fileSize}kb)|"
				th:href="@{/jpa/board/file(idx=${file.idx}, boardIdx=${file.boardIdx})}"></a>
		</div>

		<input type="button" id="list" class="btn" value="목록으로" /> <input
			type="button" id="update" class="btn" value="수정하기" /> <input
			type="button" id="delete" class="btn" value="삭제하기" />

		<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
		<script>
			$(function() {
				const boardIdx = $("#boardIdx").val();

				$("#list").on("click", function() {
					location.href = "/jpa/board";
				});
				$("#update").on("click", function() {
					$("#method").val("PUT");
					let frm = $("#frm")[0];
					frm.action = "/jpa/board/" + boardIdx;
					frm.submit();
				});
				$("#delete").on("click", function() {
					$("#method").val("DELETE");
					let frm = $("#frm")[0];
					frm.action = "/jpa/board/" + boardIdx;
					frm.submit();
				});
			});
		</script>
	</div>
</body>
</html>

jpaBoardList.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>JPA 게시판</title>
	<link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
    <div class="container">
        <h2>JPA 게시판 목록</h2>
        <table class="board_list">
            <colgroup>
                <col width="15%" />
                <col width="*" />
                <col width="15%" />
                <col width="20%" />
            </colgroup>
            <thead>
                <tr>
                    <th scope="col">글번호</th>
                    <th scope="col">제목</th>
                    <th scope="col">조회수</th>
                    <th scope="col">작성일</th>
                </tr>
            </thead>
            <tbody>
                <tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
                    <td th:text="${list.boardIdx}"></td>
                    <td class="title">
                        <a href="/jpa/board/" th:attrappend="href=${list.boardIdx}" th:text="${list.title}"></a>
                    </td>
                    <td th:text="${list.hitCnt}"></td>
                    <td th:text="${list.createdDt}"></td>
                </tr>
                <tr th:unless="${#lists.size(list)} > 0">
                    <td colspan="4">조회된 결과가 없습니다.</td>
                </tr>
            </tbody>
        </table>
        <a href="/jpa/board/write" class="btn">글쓰기</a>
    </div>
</body>
</html>

jpaBoardWrite.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>JPA 게시판</title>
    <link rel="stylesheet" th:href="@{/css/board.css}" />
</head>
<body>
    <div class="container">
        <h2>JPA 게시판 등록</h2>
        <form id="frm" method="post" action="/jpa/board/write" enctype="multipart/form-data">
	        <table class="board_detail">
	            <tr>
	               <td>제목</td>
	               <td><input type="text" id="title" name="title" /></td>
	            </tr>
	            <tr>
	               <td colspan="2"><textarea id="contents" name="contents"></textarea></td>	               
	            </tr>
	        </table>
	        <input type="file" id="files" name="files" multiple="multiple" />
	        <input type="submit" id="submit" value="저장" class="btn" />
	    </form>
    </div>
</body>
</html>

application.properties

spring.jpa.hibernate.ddl-auto=update

http://localhost:8080/jpa/board


레포지토리 인터페이스

CurdRepository

<S extends T> S save(S entity)주어진 엔티티를 저장(업데이트 포함)
<S extends T> Iterable<S> saveAll(Iterable<S> entities)주어진 엔티티 목록을 저장
Optional<T> findById(ID id) 주어진 ID로 식별된 엔티티를 반환
existsById(ID id) 주어진 ID로 식별된 엔티티의 존재 여부를 반환
Iterable<T> findAll()모든 엔티티를 반환
Iterable<T> findAllById(Iterable<ID> ids) 주어진 ID 목록에 맞는 모든 엔티티 목록을 반환
long count() 사용 가능한 엔티티의 개수를 반환
void deleteById(ID id) 주어진 ID로 식별된 엔티티를 삭제
void delete(T entity) 주어진 엔티티를 삭제
void deleteAllById(Iterable<? extends ID> ids)주어진 ID 목록으로 식별된 모든 엔티티를 삭제
void deleteAll(Iterable<? extends T> entities)주어진 엔티티 목록으로 식별된 엔티티를 모두 삭제
void deleteAll() 모든 엔티티를 삭제

PagingAndSortingRepository

Iterable<T> findAll(Sort sort)
Page<T> findAll(Pageable pageable)

⭐️⭐️⭐️ 쿼리 메서드(Query Method)

위 인터페이스를 활용

https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

접두사 + By + 필드이름 + 조건 + 정렬
~~~~~~   ~~   ~~~~~~~~   ~~~~   ~~~~
|        |    |          |      |
|        |    |          |      +-- OrderBy를 사용해 정렬 조건을 지정
|        |    |          +-- Equals, Like, GreaterThan, Between 등  
|        |    +-- 엔티티의 필드 이름 
|        +-- 필드 이름과 조건을 지정하기 전에 사용   
+-- find, read, get, query, count, delete
  • findByTitle(String title) // 쿼리 메서드
    => select jpaboard0.board_idx as board_id1_0, ... from tjpa_board jpaboard0 where jpaboard0_.ttitle = ? // JPQL

  • List<User> findByLastName(String lastName);
    => lastName 필드가 주어진 값과 일치하는 모든 User 엔티티를 조회
    => select u from User u where u.lastName = ?1 이런 쿼리가 자동으로 나간다.

  • List<User> findByLastNameAndFirstName(String lastName, String firstName);
    => lastName 필드와 firstName 필드가 모두 일치하는 User 엔티티를 조회
    => select u from User u where u.lastName = ?1 and u.firstName = ?2

  • List<User> findByFirstNameLike(String firstName);
    => select u from User u where u.firstName like ?1

  • List<User> findByFirstNameOrderByFirstNameAsc(String firstName);
    => select u from User u where u.firstName = ?1 order by u.firstName asc

  • long countByLastName(String lastName);
    => select count(u) from User u where u.lastName = ?1

  • void deleteByLastName(String lastName);
    => delete from User u where u.lastName = ?1

  • List<User> findByAgeBetween(int startAge, int endAge);
    => select u from User u where u.age between ?1 and ?2

  • List<User> findByLastNameIn(Collection<String> lastName);
    => select u from User u where u.lastName in ?1

  • List<User> findByLastNameIsNull();
    => select u from User u where u.lastName is null

@Query 사용

메서드 이름이 복잡하거나 쿼리 메서드로 표현하기 힘든 경우 ⇒ @Query 어노테이션으로 쿼리를 직접 작성
but, 가급적이면 쓰지 말자. 적용이 안 될 수도 있고 쿼리 메서드의 목적인 추상화에 걸맞지 않음

JPQL(Java Persistence Query Language) 또는 데이터베이스에 맞는 네이티브 SQL 사용이 가능 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FROM 절에 테이블 이름이 아닌 엔티티 이름을 사용 
// 순번으로 제어
@Query("select file from BoardFileEntity file where boardIdx = ?1 and idx = ?2")
BoardFileEntity findBoardFile(int boardIdx, int idx);

// 이름으로 제어
@Query("select file from BoardFileEntity file where boardIdx = :boardIdx and idx = :idx")
BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);

REST API를 JPA를 사용하도록 수정

RestApiBoardController를 복사해서 JpaRestApiBoardController 만들고 내용을 수정하고 BoartDto를 BoardEntity로 변경

JpaRestApiBoardController


package board.controller;

import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

import board.dto.BoardListResponse;
import board.entity.BoardEntity;
import board.entity.BoardFileEntity;
import board.service.JpaBoardService;
import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/api/v2")
public class JpaRestApiBoardController {

    @Autowired
    private JpaBoardService boardService;

    // 목록 조회
    @GetMapping("/board")
    public ResponseEntity<Object> openBoardList() throws Exception {
        List<BoardListResponse> results = new ArrayList<>();

        try {
            List<BoardEntity> boardList = boardService.selectBoardList();
            boardList.forEach(dto -> results.add(new ModelMapper().map(dto, BoardListResponse.class)));
            return new ResponseEntity<>(results, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("목록 조회 실패", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    // 저장 처리
    @PostMapping(value = "/board", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<Object> insertBoard(@RequestParam("board") String boardData, MultipartHttpServletRequest request) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        BoardEntity boardDto = objectMapper.readValue(boardData, BoardEntity.class);
        Map<String, String> result = new HashMap<>();
        try {
            boardService.insertBoard(boardDto, request);
            result.put("message", "게시판 저장 성공");
            return ResponseEntity.status(HttpStatus.CREATED).body(result);
        } catch(Exception e) {
            result.put("message", "게시판 저장 실패");
            result.put("description", e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }

    }

    // 상세 조회
    @GetMapping("/board/{boardIdx}")
    public ResponseEntity<Object> openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception {
        BoardEntity boardEntity = boardService.selectBoardDetail(boardIdx);
        if (boardEntity == null) {
            Map<String, Object> result = new HashMap<>();
            result.put("code", HttpStatus.NOT_FOUND.value());
            result.put("name", HttpStatus.NOT_FOUND.name());
            result.put("message", "게시판 번호 " + boardIdx + "와 일치하는 게시물이 존재하지 않습니다.");
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(result);
        } else {
            return ResponseEntity.status(HttpStatus.OK).body(boardEntity);
        }
    }

    // 수정 처리
    @PutMapping("/board/{boardIdx}")
    public void updateBoard(@PathVariable("boardIdx") int boardIdx, @RequestBody BoardEntity boardEntity) throws Exception {
        boardEntity.setBoardIdx(boardIdx);
        boardService.updateBoard(boardEntity);
    }

    // 삭제 처리
    @DeleteMapping("/board/{boardIdx}")
    public void deleteBoard(@PathVariable("boardIdx") int boardIdx) throws Exception {
        boardService.deleteBoard(boardIdx);
    }

    // 첨부파일 다운로드
    @GetMapping("/board/file")
    public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
        BoardFileEntity boardFileEntity = boardService.selectBoardFileInfo(idx, boardIdx);
        if (ObjectUtils.isEmpty(boardFileEntity)) {
            return;
        }

        Path path = Paths.get(boardFileEntity.getStoredFilePath());
        byte[] file = Files.readAllBytes(path);

        response.setContentType("application/octet-stream");
        response.setContentLength(file.length);
        response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(boardFileEntity.getOriginalFileName(), "UTF-8") + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.getOutputStream().write(file);
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }
}

실습

이전 프로젝트 결과물을 JPA 기반의 REST API로 변경해 보세요.

0개의 댓글

관련 채용 정보