article thumbnail image
Published 2022. 11. 23. 14:50


 

 

스프링 데이터 JPA를 위한 기본 설정

 

application.properties 설정 추가하기

spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.generate-ddl=true
spring.jpa.hibernate.use-new-id-generator-mappings=false

1: 사용할 데이터베이스를 MySQL로 설정

2: MySQL은 InnoDB, MyIsAM 등 여러 가지 엔진을 지원한다. 그중 일반적으로 많이 사용하는 엔진은 InnoDB로, MyISAM에 비해서 성능 및 트랜잭션 지원 등에서 장점이 많다. 설정하지 않으면 기본값은 MyISAM으로 된다.

3: JPA의 엔티티 연관관계를 바탕으로 테이블 생성과 같은 스크립트를 자동으로 실행하도록 한다. 여기서는 JPA의 기능을 알아보기 위해 ture 옵션을 사용했지만 실제 개발에서는 이 기능은  꼭 false로 사용 해야 한다. 개발자의 실수 등으로 예상치 못하게 데이터베이스에 변경이 생기면서 데이터가 삭제될 수 있기 때문.

4: 하이버네이트의 새로운 ID 생성 옵션의 사용 여부를 설정한다. 하이버네이트의 ID 생성 옵션은 AUTO, TABLE, SEQUENCE가 있다. 여기서는 MySQL의 자동증가 속성을 사용하기 때문에 false로 설정한다.

 

 

 

DatabaseConfiguration.java 설정 추가하기

	@ConfigurationProperties(prefix="spring.jpa")
	public Properties hibernateConfig(){
		return new Properties();
	}

 

 

 

build.gradle 설정 추가하기

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

JPA 에 필요한 정보를 import 하는데 에러가 나서 찾아보니 build.gradle에 설정이 빠졌다.

 

 

 

 

자바 8의 날짜 API 설정하기

BoardApplication.java 설정 추가하기

@EnableJpaAuditing
@EntityScan(
		basePackageClasses = {Jsr310JpaConverters.class},
		basePackages = {"board"})

자바8 에는 자바7 이하에서 문제가 되었던 시간 관련 클래스들이 추가되었다. 자바7 이하에서는 시간 관련 클래스를 사용하기 불편하다는 악평이 자자했다. 그래서 사람들은 이런 문제를 개선한 Joda-Time과 같은 라이브러리를 사용했다.

자바8 에서는 JSR-310 이라는 표준 명세로 날짜와 시간에 관련된 새로운 API를 추가했다. 그렇지만 자바8의 날짜 및 시간 관련 클래스를 그대로 사용할 경우 환경에 따라서 문제가 발생할 수 있다. 이 문제를 해결하기 위한 방법이 몇 가지 있는데, 여기서는 가장 간단하게 사용할 수 있는 Jsr310JpaConverters 적용 방법을 사용했다.

 

 

 

 

 

JPA를 사용한 게시판으로 변경

 

게시판 엔티티 생성하기

entity 패키지새로 생성

board\src\main\java\board\board\entity\BoardEntity.java

package board.board.entity;

import java.time.LocalDateTime;
import java.util.Collection;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name="t_jpa_board")
@NoArgsConstructor
@Data
public class BoardEntity {
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private int boardIdx;

	@Column(nullable=false)
	private String title;
	
	@Column(nullable=false)
	private String contents;
	
	@Column(nullable=false)
	private int hitCnt = 0;
	
	@Column(nullable=false)
	private String creatorId;
	
	@Column(nullable=false)
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
	private LocalDateTime createdDatetime = LocalDateTime.now();
	
	private String updaterId;
	
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
	private LocalDateTime updatedDatetime;

	@OneToMany(cascade=CascadeType.ALL)
	@JoinColumn(name="board_idx")
	private Collection<BoardFileEntity> fileList;
}

21: @Entity 어노테이션은 해당 클래스가JPA의 엔티티임을 나타낸다. 엔티티 클래스는 테이블과 매핍된다.

22: t_jpa_board 테이블과 매핑되도록 나타낸다.

26: 엔티티의 기본키(PK) 임을 나타낸다.

27: 기본키의 생성 전략을 설정한다. GenerationType.AUTO로 지정할 경우 데이터베이스에서 제공하는 기본키 생성 전략을 따르게 된다. MySQL은 자동 증가를 지원하므로 기본키가 자동으로 증가하며, 자동 증가가 지원되지 않는 오라클의 경우 기본키에 사용할 시퀀스를 생성하게 된다.

30: 컬럼에 Not Null 속성을 지정한다.

51~53: @OneToMany는 1:N의 관계를 표현하는 JPA 어노테이션이다. 하나의 게시글은 첨부파일이 없더나 1개 이상일 수도 있다. @JoinColumn 어노테이션은 릴레이션 관계가 있는 테이블의 컬럼을 지정한다.

fetch=FetchType.EAGER은 관계에 필요한 데이터를 바로 조회할것이냐 인데, 에러가 나서 뺏다. 아마 기본값이 해당값이라 그런것으로 보인다.

 

 

 

 

파일 엔티티 생성

board\src\main\java\board\board\entity\BoardFileEntity.java

package board.board.entity;

import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name="t_jpa_file")
@NoArgsConstructor
@Data
public class BoardFileEntity {
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private int idx;
	
	@Column(nullable=false)
	private String originalFileName;
	
	@Column(nullable=false)
	private String storedFilePath;
	
	@Column(nullable=false)
	private long fileSize;

	@Column(nullable=false)
	private String creatorId;
	
	@Column(nullable=false)
	private LocalDateTime createdDatetime = LocalDateTime.now();
	
	private String updaterId;
	
	private LocalDateTime updatedDatetime;
}

 

 

 

 

컨트롤러 생성

board\src\main\java\board\board\controller\JpaBoardController.java

package board.board.controller;

import java.io.File;
import java.net.URLEncoder;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;

import board.board.entity.BoardEntity;
import board.board.entity.BoardFileEntity;
import board.board.service.JpaBoardService;

@Controller
public class JpaBoardController {
	
	@Autowired
	private JpaBoardService jpaBoardService;
	
	@RequestMapping(value="/jpa/board", method=RequestMethod.GET)
	public ModelAndView openBoardList(ModelMap model) throws Exception{
		ModelAndView mv = new ModelAndView("/board/jpaBoardList");
		
		List<BoardEntity> list = jpaBoardService.selectBoardList();
		mv.addObject("list", list);
		
		return mv;
	}
	
	@RequestMapping(value="/jpa/board/write", method=RequestMethod.GET)
	public String openBoardWrite() throws Exception{
		return "/board/jpaBoardWrite";
	}
	
	@RequestMapping(value="/jpa/board/write", method=RequestMethod.POST)
	public String writeBoard(BoardEntity board, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
		jpaBoardService.saveBoard(board, multipartHttpServletRequest);
		return "redirect:/jpa/board";
	}
	
	@RequestMapping(value="/jpa/board/{boardIdx}", method=RequestMethod.GET)
	public ModelAndView openBoardDetail(@PathVariable("boardIdx") int boardIdx) throws Exception{
		ModelAndView mv = new ModelAndView("/board/jpaBoardDetail");
		
		BoardEntity board = jpaBoardService.selectBoardDetail(boardIdx);
		mv.addObject("board", board);
		
		return mv;
	}
	
	@RequestMapping(value="/jpa/board/{boardIdx}", method=RequestMethod.PUT)
	public String updateBoard(BoardEntity board) throws Exception{
		jpaBoardService.saveBoard(board, null);
		return "redirect:/jpa/board";
	}
	
	@RequestMapping(value="/jpa/board/{boardIdx}", method=RequestMethod.DELETE)
	public String deleteBoard(@PathVariable("boardIdx") int boardIdx) throws Exception{
		jpaBoardService.deleteBoard(boardIdx);
		return "redirect:/jpa/board";
	}
	
	@RequestMapping(value="/jpa/board/file", method=RequestMethod.GET)
	public void downloadBoardFile(int boardIdx, int idx, HttpServletResponse response) throws Exception{
		BoardFileEntity file = jpaBoardService.selectBoardFileInformation(boardIdx, idx); 
		
		byte[] files = FileUtils.readFileToByteArray(new File(file.getStoredFilePath()));
		
		response.setContentType("application/octet-stream");
		response.setContentLength(files.length);
		response.setHeader("Content-Disposition", "attachment; fileName=\"" + URLEncoder.encode(file.getOriginalFileName(),"UTF-8")+"\";");
		response.setHeader("Content-Transfer-Encoding", "binary");
		
		response.getOutputStream().write(files);
		response.getOutputStream().flush();
		response.getOutputStream().close();
	}
}

전체적으로 RestBoardController 와 동일하다. 단지 URI가 겹치지 않게 했다. URI가 변경되었으니 뷰에서 호출하는 URI도 변경되어야 해서 뷰도 만들어야 한다.

기존의 BoardDto와 BoardFileDto 대신 각각 BoardEntity, BoardFileEntity로 변경한다.

 

게시글을 작성할 떄와 수정할 때 모두 동일한 save 서시브 메서드를 호출한다.

 

 

 

 

서비스 생성

board\src\main\java\board\board\service\JpaBoardService.java

package board.board.service;

import java.util.List;

import org.springframework.web.multipart.MultipartHttpServletRequest;

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

public interface JpaBoardService {

	List<BoardEntity> selectBoardList() throws Exception;

	void saveBoard(BoardEntity board, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception;
	
	BoardEntity selectBoardDetail(int boardIdx) throws Exception;

	void deleteBoard(int boardIdx);

	BoardFileEntity selectBoardFileInformation(int boardIdx, int idx) throws Exception;
}

 

 

board\src\main\java\board\board\service\JpaBoardServiceImpl.java

package board.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.board.entity.BoardEntity;
import board.board.entity.BoardFileEntity;
import board.board.repository.JpaBoardRepository;
import board.common.FileUtils;

@Service
public class JpaBoardServiceImpl implements JpaBoardService{
	
	@Autowired
	JpaBoardRepository jpaBoardRepository;
	
	@Autowired
	FileUtils fileUtils;

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

	@Override
	public void saveBoard(BoardEntity board, MultipartHttpServletRequest multipartHttpServletRequest) throws Exception {
		board.setCreatorId("admin");
		List<BoardFileEntity> list = fileUtils.parseFileInfo(multipartHttpServletRequest);
		if(CollectionUtils.isEmpty(list) == false){
			board.setFileList(list);
		}
		jpaBoardRepository.save(board);
	}
	
	@Override
	public BoardEntity selectBoardDetail(int boardIdx) throws Exception{
		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 NullPointerException();
		}
	}

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

	@Override
	public BoardFileEntity selectBoardFileInformation(int boardIdx, int idx) throws Exception {
		BoardFileEntity boardFile = jpaBoardRepository.findBoardFile(boardIdx, idx);
		return boardFile;
	}
}

 

 

 

 

FileUtil 클래스 변경

 

게시글을 저장하는 JpaBoardServiceImpl 클래스의 saveBoard 메서드를 설명 할 때 parseBoardFile 메서드를 사용했다. 이 메서더는 앞에서 만든 parseBoardFile 메서드와 기능은 동일하지만 첨부파일의 정보를 BoardFileDto 클래스 대신 BoardFileEntity 클래스로 이용한다는 점이 다르다. parseBoardFile메서드와 동일하게 작성하고 진하게 표시된 부분만 변경한다.

board\src\main\java\board\common\FileUtils.java

	public List<BoardFileEntity> parseFileInfo(MultipartHttpServletRequest multipartHttpServletRequest) throws Exception{
		if(ObjectUtils.isEmpty(multipartHttpServletRequest)){
			return null;
		}
		
		List<BoardFileEntity> fileList = new ArrayList<>();
		
		DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMdd"); 
    	ZonedDateTime current = ZonedDateTime.now();
    	String path = "images/"+current.format(format);
    	File file = new File(path);
		if(file.exists() == false){
			file.mkdirs();
		}
		
		Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
		
		String newFileName, originalFileExtension, contentType;
		
		while(iterator.hasNext()){
			List<MultipartFile> list = multipartHttpServletRequest.getFiles(iterator.next());
			for (MultipartFile multipartFile : list){
				if(multipartFile.isEmpty() == false){
					contentType = multipartFile.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;
						}
					}
					
					newFileName = Long.toString(System.nanoTime()) + originalFileExtension;
					BoardFileEntity boardFile = new BoardFileEntity();
					boardFile.setFileSize(multipartFile.getSize());
					boardFile.setOriginalFileName(multipartFile.getOriginalFilename());
					boardFile.setStoredFilePath(path + "/" + newFileName);
					boardFile.setCreatorId("admin");
					fileList.add(boardFile);
					
					file = new File(path + "/" + newFileName);
					multipartFile.transferTo(file);
				}
			}
		}
		return fileList;
	}

 

 

 

 

 

리포지터리 작성하기

 

리포지터리는 스프링 데이터 JPA가 제공하는 인터페이스다. 스프링 데이터 JPA가 제공하는 리포지터리 인터페이스는 몇 가지 종류가 있다. 여기는 가장 간단하게 사용할 수 있는 CrudRepository 인터페이스를 사용해보자. 

repository 패키지를 만들고 JpaBoardRepository인터페이스를 생성해보자.

package board.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 board.board.entity.BoardEntity;
import board.board.entity.BoardFileEntity;

public interface JpaBoardRepository extends CrudRepository<BoardEntity, Integer>{

	List<BoardEntity> findAllByOrderByBoardIdxDesc();
	
	@Query("SELECT file FROM BoardFileEntity file WHERE board_idx = :boardIdx AND idx = :idx")
	BoardFileEntity findBoardFile(@Param("boardIdx") int boardIdx, @Param("idx") int idx);
}

 

 

 

뷰 생성하기

jpaBoardList.html, jpaBoardWrite.html, jpaBoardDetail.html

 

REST에서 사용했던 html을 복사하고 URI만 변경한다.

 

 

 

 

 

 

8장을 마치며,,

JPA를 사용해보고 변경되는 사항들을 직접해봤다. JPA특징에 대해서 알아봤고 설정하는 방법에 대해서 알게되었다.

'Spring Boot > 6. JPA' 카테고리의 다른 글

JPA 정의  (0) 2022.11.23
복사했습니다!