TIL 0624

먼지·2024년 6월 24일
0

Today I Learned

목록 보기
83/89
post-thumbnail

이론

Spring Boot 설정하기

pom.xml

버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경, 라이브러리 추가
pom.xml에서 오류가 발생한다면 maven update 실행

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.17</version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.3.1</version>
</dependency>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter-test</artifactId>
	<version>2.3.1</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>jstl</artifactId>
</dependency>

<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

src/main/resources > application.yml

계층적인 형태로 작성하는 것이 더 편하기 때문에 yml 코드를 작성함

spring: 
  profiles: #실행환경(local(로컬),dev(개발),real(운영))
    active: local
  
  mvc:
    view: #view 경로 및 확장자 지정 (우리의 view 코드는 views 폴더에 포함이 되어있고, 뷰는 jsp로 항상 입력)
      prefix: /WEB-INF/views/
      suffix: .jsp

  datasource: #DB 접속
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@localhost:1521:xe
    username: user01
    password: 1234
    hikari: #기본 커넥션풀 
      connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
      maximum-pool-size : 10 #최대 커넥션 개수 설정
      max-lifetime: 1800000  #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
      
  messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
    basename: messages.validation
    encoding: UTF-8
    
  devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
    livereload:
      enabled: true
    
    restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
      enabled: true

mybatis: #mybatis 설정
  type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
  mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
  #mapper-locations: mybatis/mapper/**/*.xml 
  
logging: #로그 지정
  level:
    #root: error
    '[kr.spring]': debug 
    
server:
  port: 8000
  servlet:
    #context-path: /mybatis
    encoding:
      charset: UTF-8
      enabled: true #http 인코딩 지원을 활성화할지 여부
      force: true

application.properties

application properties 를 사용하고 싶다면 해당 코드를 작성하면 된다

#tomcat port setting
server.port=8000
#JSP setting (main 밑에 webapp/WEB-INF/views 폴더 생성 후)
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
#oracle setting
spring.datasource.hikari.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.hikari.jdbc-url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.hikari.username=scott
spring.datasource.hikari.password=tiger
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
#설정한 시간을 초과하면 SQLException이 발생
spring.datasource.hikari.connection-timeout=30000
#connection pool에서 유지가능한 최소 커넥션 개수를 설정합니다.
#최적의 성능과 응답성을 원하면 이 값을 설정하지 않는게 좋다고 함
#spring.datasource.hikari.minimum-idle=5

#connection의 최대 유지 시간을 설정합니다.
#connection의 maxLifeTime 지났을 때, 사용중인 connection은 바로 폐기되지않고 작업이 완료되면 폐기됩니다.
#하지만 유휴 커넥션은 바로 폐기됩니다.
spring.datasource.hikari.max-lifetime=240000
#유휴 및 사용중인 connection을 포함하여 풀에 보관가능한 최대 커넥션 개수를 설정
#사용할 수 있는 커넥션이 없다면 connectionTimeout 시간 만큼 대기하고 시간을 초과하면 SQLException이 발생
spring.datasource.hikari.maximum-pool-size=20
#spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
#spring.datasource.username=scott
#spring.datasource.password=tiger

spring.messages.basename=messages.validation

#JSP reload
spring.devtools.livereload.enabled=true

#mybatis setting
mybatis.mapper-locations=classpath:mybatis/mapper/**/*.xml
mybatis.type-aliases-package=kr.spring.**.vo

#log setting(default->info)
logging.level.kr.spring=DEBUG

server.servlet.context-path:/

실습

ch12 Spring Mybatis Boot

index.jsp는 실행이 불가능하다. MVC 패턴일때만 index.jsp를 통하여 호출
지금은 새로운 방식으로 설정을 할 예정

정적 파일의 위치

kr.spring.board.vo

Board VO

package kr.spring.board.vo;

import java.sql.Date;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

public class BoardVO {
	
	private int num;
	@NotBlank
	private String writer;
	@NotBlank
	private String title;
	@NotBlank
	private String passwd;
	@NotEmpty
	private String content;
	private Date reg_date;
	
	public int getNum() {
		return num;
	}
	public void setNum(int num) {
		this.num = num;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getPasswd() {
		return passwd;
	}
	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public Date getReg_date() {
		return reg_date;
	}
	public void setReg_date(Date reg_date) {
		this.reg_date = reg_date;
	}
	@Override
	public String toString() {
		return "BoardVO [num=" + num + ", writer=" + writer + ", title=" + title + ", passwd=" + passwd + ", content="
				+ content + ", reg_date=" + reg_date + "]";
	}
	
	
}

kr.spring.board.dao

Board Mapper

Interface 파일
@Mapper 어노테이션을 작성하지 않으면 자동으로 생성하지 못하기 때문에 꼭 작성해줘야 한다.

package kr.spring.board.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import kr.spring.board.vo.BoardVO;

@Mapper
public interface BoardMapper {
	
		@Insert("INSERT INTO aboard VALUES(aboard_seq.nextval,#{writer},#{title},#{passwd},#{content},SYSDATE)")
		public void insertBoard(BoardVO board);
		
		@Select("SELECT COUNT(*) FROM aboard")
		public int getBoardCount();
		
		public List<BoardVO> getBoardList(Map<String, Integer> map);
		
		@Select("SELECT * FROM aboard WHERE num=#{num}")
		public BoardVO selectBoard(int num);
		
		@Update("UPDATE aboard SET writer=#{writer}, title=#{title}, content=#{content} WHERE num=#{num}")
		public void updateBoard(BoardVO board);
		
		@Delete("DELETE FROM aboard WHERE num=#{num}")
		public void deleteBoard(int num);
		
		
}

Board Mapper XML

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper   
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  

<!-- interface명칭과 xml의 명칭이 동일해야 한다 -->
<mapper namespace="kr.spring.board.dao.BoardMapper">
	<!-- 목록 가져오기 , 아이디는 따로 명시하지 않았음 그러나 메소드 명을 아이디로 사용 가능함-->
 	<select id="getBoardList" parameterType="map" resultType="boardVO">
		SELECT * FROM(SELECT a.*,rownum rnum FROM (SELECT * FROM aboard ORDER BY num DESC)a) 
		<![CDATA[
		WHERE rnum >= #{start} AND rnum <= #{end}
		]]>
	</select>
	
 </mapper>

kr.spring.board.service

Board Service

package kr.spring.board.service;

import java.util.List;
import java.util.Map;

import kr.spring.board.vo.BoardVO;

public interface BoardService {
	public void insertBoard(BoardVO board);
	public int getBoardCount();
	public List<BoardVO> getBoardList(Map<String, Integer> map);
	public BoardVO selectBoard(int num);
	public void updateBoard(BoardVO board);
	public void deleteBoard(int num);
}

Board Service Impl

package kr.spring.board.service;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import kr.spring.board.dao.BoardMapper;
import kr.spring.board.vo.BoardVO;

@Service
@Transactional
public class BoardServiceImpl implements BoardService{

	@Autowired
	private BoardMapper boardMapper;
	
	@Override
	public void insertBoard(BoardVO board) {
		boardMapper.insertBoard(board);
	}
	
	@Override
	public int getBoardCount() {
		return boardMapper.getBoardCount();
	}

	@Override
	public List<BoardVO> getBoardList(Map<String, Integer> map) {
		return boardMapper.getBoardList(map);
	}

	@Override
	public BoardVO selectBoard(int num) {
		return boardMapper.selectBoard(num);
	}

	@Override
	public void updateBoard(BoardVO board) {
		boardMapper.updateBoard(board);
	}

	@Override
	public void deleteBoard(int num) {
		boardMapper.deleteBoard(num);
	}
}

Ch12SpringMyBatisBootApplication.java

@SpringBootApplication
이 어노테이션은 여러 어노테이션을 합친 것으로, @Configuration, @EnableAutoConfiguration,@ComponentScan을 포함한다. 이 어노테이션이 있으면 스프링 부트는 애플리케이션 설정을 자동으로 구성하고, 컴포넌트를 스캔하여 필요한 빈(bean)을 자동으로 등록한다.

이 메소드는 자바 애플리케이션의 시작 지점이다.

SpringApplication.run(Ch12SpringMybatisBootApplication.class, args);

스프링 부트 애플리케이션을 시작하고, 내장된 서버(톰캣)를 실행합니다.

package kr.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Ch12SpringMybatisBootApplication {

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

}
  1. main 메소드가 실행
  2. SpringApplication.run 메소드는 스프링 부트 애플리케이션을 초기화하고 실행
  3. @SpringBootApplication 어노테이션 덕분에 스프링 부트는 설정, 빈 등록 등을 자동으로 처리된다.
  4. 애플리케이션이 시작되면, 스프링 부트는 @Controller, @RestController, @Service, @Repository 등의 어노테이션이 붙은 클래스를 스캔하고 관리한다.
  5. URL 요청이 들어오면, 스프링 부트는 해당 URL과 매핑된 컨트롤러 메소드를 찾아 실행한다.

kr.spring.board.controller

Board Controller

초기 진입에 대한 요청

@RequestMapping("/")은 웹 애플리케이션의 시작점을 설정하는 역할이기 때문에 Ch12SpringMyBatisBootApplication.java의 마지막 부분인 url 요청의 역할을 한다고 보면 된다. 그래서 해당 프로젝트를 실행시키면 list.do가 실행되는 것이다.

	@RequestMapping("/")
	public String main() {
		return "redirect:/list.do";
	}

전체 코드

package kr.spring.board.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.ModelAndView;

import kr.spring.board.service.BoardService;
import kr.spring.board.vo.BoardVO;
import kr.spring.util.PagingUtil;

@Controller
public class BoardController {
	@Autowired
	private BoardService boardService;
	//로그 처리(로그 대상 지정)
	private static final Logger log = 
			LoggerFactory.getLogger(BoardController.class);
	
	//유효성 체크를 위한 자바빈(VO) 초기화
	@ModelAttribute
	public BoardVO initCommand(){
		return new BoardVO();
	}
	
	// 초기 진입에 대한 요청 넣기
	@RequestMapping("/")
	public String main() {
		return "redirect:/list.do";
	}
	
	//글쓰기 폼 호출
	@GetMapping("/insert.do")
	public String form() {
		return "insertForm";
	}
	//전송된 데이터 처리
	@PostMapping("/insert.do")
	public String submit(@Valid BoardVO vo, 
			                  BindingResult result) {
		log.debug("<<BoardVO>> : " + vo);
		
		//유효성 체크 결과 오류가 있으면 폼 호출
		if(result.hasErrors()) {
			return form();
		}
		//글쓰기
		boardService.insertBoard(vo);
		
		return "redirect:/list.do";
	}
	
	//목록
	@RequestMapping("/list.do")
	public ModelAndView getList(
			@RequestParam(value="pageNum",
			          defaultValue="1") int currentPage) {
		//총레코드 수
		int count = boardService.getBoardCount();
		//페이지 처리
		PagingUtil page = 
			new PagingUtil(currentPage,count,20,10,"list.do");
		
		//목록 호출
		List<BoardVO> list = null;
		if(count > 0) {
			Map<String,Integer> map = 
					       new HashMap<String,Integer>();
			map.put("start", page.getStartRow());
			map.put("end", page.getEndRow());
			list = boardService.getBoardList(map);
		}
		
		ModelAndView mav = new ModelAndView();
		//뷰 이름 설정
		mav.setViewName("selectList");
		//데이터 저장
		mav.addObject("count", count);
		mav.addObject("list", list);
		mav.addObject("page", page.getPage());
		
		return mav;
	}
	//글상세
	@RequestMapping("/detail.do")
	public ModelAndView detail(int num) {
		BoardVO board = 
				boardService.selectBoard(num);
		return new ModelAndView("selectDetail","board",board);
	}
	//수정 폼 호출
	@GetMapping("/update.do")
	public String formUpdate(int num,Model model) {
		model.addAttribute("boardVO", 
				boardService.selectBoard(num));
		
		return "updateForm";
	}
	//전송된 데이터 처리
	@PostMapping("/update.do")
	public String submitUpdate(@Valid BoardVO vo,
			                   BindingResult result) {
		log.debug("<<BoardVO>> : " + vo);
		
		//유효성 체크 결과 오류가 있으면 폼 호출
		if(result.hasErrors()) {
			return "updateForm";
		}
		
		//비밀번호 일치 여부 체크
		//DB에 저장된 비밀번호 구하기
		BoardVO db_board = 
				boardService.selectBoard(vo.getNum());
		//비밀번호 체크
		if(!db_board.getPasswd().equals(vo.getPasswd())) {
			result.rejectValue("passwd", "invalidPassword");
			return "updateForm";
		}
		
		//글 수정
		boardService.updateBoard(vo);		
		
		return "redirect:/list.do";
	}
	//글 삭제 폼 호출
	@GetMapping("/delete.do")
	public String formDelete(BoardVO vo) {
		return "deleteForm";
	}
	//글 삭제 처리
	@PostMapping("/delete.do")
	public String submitDelete(@Valid BoardVO vo,
			                   BindingResult result) {
		log.debug("<<BoardVO>> : " + vo);
		
		//유효성 체크 결과 오류가 있으면 폼 호출
		//비밀번호 전송 여부만 체크
		if(result.hasFieldErrors("passwd")) {
			return "deleteForm";
		}
		//비밀번호 일치 여부 체크
		//DB에 저장된 비밀번호 구하기
		BoardVO db_board = 
				boardService.selectBoard(vo.getNum());
		//비밀번호 체크
		if(!db_board.getPasswd().equals(vo.getPasswd())) {
			result.rejectValue("passwd", "invalidPassword");
			return "deleteForm";
		}
		//글 삭제
		boardService.deleteBoard(vo.getNum());
		
		return "redirect:/list.do";
	}
}


Ch13 Spring Tiles

스프링 부트 Tiles 설정

pom.xml

버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경, 라이브러리 추가
pom.xml에서 오류가 발생한다면 maven update 실행

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.17</version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.3.1</version>
</dependency>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter-test</artifactId>
	<version>2.3.1</version>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>jstl</artifactId>
</dependency>

<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

<dependency>
	<groupId>org.apache.tiles</groupId>
	<artifactId>tiles-servlet</artifactId>
	<version>3.0.7</version>
</dependency>

<dependency>
	<groupId>org.apache.tiles</groupId>
	<artifactId>tiles-jsp</artifactId>
	<version>3.0.7</version>
</dependency>

src/main/resources > application.yml

계층적인 형태로 작성하는 것이 더 편하기 때문에 yml 코드를 작성함

spring: 
  profiles: #실행환경(local(로컬),dev(개발),real(운영))
    active: local
  
  mvc:
    view: #view 경로 및 확장자 지정 (우리의 view 코드는 views 폴더에 포함이 되어있고, 뷰는 jsp로 항상 입력)
      prefix: /WEB-INF/views/
      suffix: .jsp

  datasource: #DB 접속
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@localhost:1521:xe
    username: user01
    password: 1234
    hikari: #기본 커넥션풀 
      connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
      maximum-pool-size : 10 #최대 커넥션 개수 설정
      max-lifetime: 1800000  #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
      
  messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
    basename: messages.validation
    encoding: UTF-8
    
  devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
    livereload:
      enabled: true
    
    restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
      enabled: true

mybatis: #mybatis 설정
  type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
  mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
  #mapper-locations: mybatis/mapper/**/*.xml 
  
logging: #로그 지정
  level:
    #root: error
    '[kr.spring]': debug 
    
server:
  port: 8000
  servlet:
    #context-path: /mybatis
    encoding:
      charset: UTF-8
      enabled: true #http 인코딩 지원을 활성화할지 여부
      force: true

kr.spring.config

Spring Framework에서 Tiles를 사용하여 View를 구성하는 방법을 설정하는 Java 설정 파일

AppConfig

  • @Configuration
    - 해당하는 클래스가 Spring의 Java 기반 설정 파일임을 나타내는 어노테이션

  • public class AppConfig implements WebMvcConfigurer
    - WebMvcConfigurer 인터페이스를 구현한 클래스
    - 이를 통해 Spring MVC의 구성을 추가적으로 설정

  • @Bean
    - 메서드 레벨에서 해당 애너테이션이 사용되면, 해당 메서드가 Spring 컨테이너에 의해 관리되는 빈(Bean)을 정의한다는 것을 나타냄

  • TilesConfigurer tilesConfigurer()
    - TilesConfigurer 객체를 생성하고 반환하는 메서드
    - TilesConfigurer 는 Tiles 설정을 관리하는 데 사용된다.

  • setDefinitions(new String[] {"/WEB-INF/tiles-def/tilesdef.xml"})
    - Tiles의 정의 파일 경로를 설정한다.
    - /WEB-INF/tiles-def/tilesdef.xml 파일에는 Tiles의 레이아웃 및 타일 정의가 포함되어 있어야 한다.

  • setCheckRefresh(true)
    - 설정 파일을 변경할 때마다 Tiles 설정을 새로 고침할지 여부를 설정

  • TilesViewResolver tilesViewResolver()
    - TilesViewResolver 객체를 생성하고 반환하는 메서드
    - 이 Resolver는 Tiles 뷰를 해석하여 반환

  • setViewClass(TilesView.class)
    - 이 Resolver가 사용할 뷰 클래스를 TilesView로 설정한다.
    - 이 설정 파일을 사용하면 Spring MVC 애플리케이션에서 Tiles를 통해 레이아웃 기반의 View 구성을 쉽게 할 수 있습니다.
    - /WEB-INF/tiles-def/tilesdef.xml 파일에는 Tiles의 정의가 포함되어 있어야 하며, 이 파일에서는 각 타일의 레이아웃과 실제 뷰를 매핑할 수 있다.

package kr.spring.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;

@Configuration
public class AppConfig implements WebMvcConfigurer{

	@Bean	
	public TilesConfigurer tilesConfigurer() {
		final TilesConfigurer configurer = new TilesConfigurer();
		
		// 해당 경로에 tiles.xml 파일을 넣음
		configurer.setDefinitions(new String[] {"/WEB-INF/tiles-def/tilesdef.xml"});
		configurer.setCheckRefresh(true);
		return configurer;
	}
	
	@Bean
	public TilesViewResolver tilesViewResolver() {
		final TilesViewResolver tilesViewResolver = new TilesViewResolver();
		tilesViewResolver.setViewClass(TilesView.class);
		
		return tilesViewResolver;
	}
}

webapp > WEB-INF > tiles-def

tilesdef XML

main을 설정하는 기본 레이아웃과 title, body 외에는 모두 동일하기 때문에 main에서 바뀌지 않는 설정들을 상속을 받고 바뀌는 부분들만 작성해주면 된다.

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">

<tiles-definitions>
	<!-- 기본 레이아웃 설정 -->
	<definition name="main" template="/WEB-INF/views/template/layout.jsp">
		<put-attribute name="title" value="Spring Tiles Example"/>
		<put-attribute name="header" value="/WEB-INF/views/template/header.jsp" />
		<put-attribute name="menu" value="/WEB-INF/views/template/menu.jsp" />
		<put-attribute name="body" value="/WEB-INF/views/main_view.jsp" />
		<put-attribute name="footer" value="/WEB-INF/views/template/footer.jsp" />
	</definition>
  
	<!-- 기존 설정을 그대로 유지하는데 바뀌는 것만 명시 -->
	<definition name="company" extends="main">
		<put-attribute name="title" value="Company Introduction"/>
		<put-attribute name="body" value="/WEB-INF/views/company_view.jsp" />
	</definition>
  
	<definition name="product" extends="main">
		<put-attribute name="title" value="Product Introduction"/>
		<put-attribute name="body" value="/WEB-INF/views/product_view.jsp" />
	</definition>
  
</tiles-definitions>

WEB-INF > views > template

footer에 위치하는 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<div align="center" style="background: yellow;">
	&copy; Spring Tiles Example
</div>

Header.jsp

header에 위치하는 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<h2 align="center">Spring Tiles Example!</h2>

menu에 위치하는 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<ul>
  <li><a href="main.do">main</a></li>
  <li><a href="company.do">회사 소개</a></li>
  <li><a href="product.do">제품 소개</a></li>
</ul>

Layout.jsp

header, menu, body, footer를 모두 모아주는 코드

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title> <tiles:insertAttribute name="title" /> </title>
</head>
<body>
<table style="width: 100%;">
	<tr height="100" valign="middle" bgcolor="#ffffff">
		<td colspan="2"><tiles:insertAttribute name="header"/></td>
	</tr>
	
	<tr height="570">
		<td width="15%" valign="top">
			<tiles:insertAttribute name="menu"/>
		</td>
		<td width="85%" align="center">
			<tiles:insertAttribute name="body"/>
		</td>
	</tr>
	
	<tr>
		<td colspan="2"><tiles:insertAttribute name="footer"/></td>
	</tr>
	
</table>
</body>
</html>

WEB-INF > views

main_view.jsp

body에 들어오는 내용은 각 페이지마다 다르기 때문에 template 안에 넣지 않고 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<b>Main Page Body</b>

company_view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<b>Company Introduction</b>

product_view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<b>Product Introduction</b>

kr.spring.tiles.controller

Tiles Controller

첫 화면

ch13SpringTiles를 실행시켰을 때 처음으로 뜨는 화면을 지정해주는 코드이다.
여기서는 main.do를 실행하도록 return에서 페이지를 정해준다.

@RequestMapping("/")
	public String init() {
		return "redirect:/main.do";
	}

main 진입

main이 실행되도록 @RequestMapping("/main.do")에 해당하는 코드를 작성해준다.
return "main"; 해당 코드의 main은 tilesdef.xml에서 definition 태그의 name의 명칭과 동일해야 한다.

@RequestMapping("/main.do")
	public String main() {
		return "main";
	}

company 진입

company.do 호출하는 코드
return "company"; 해당 코드의 company는 tilesdef.xml에서 definition 태그의 name 명칭과 동일

@RequestMapping("/company.do")
	public String viewCompany() {
		return "company";
	}

product 진입

product.do 호출하는 코드
return "product"; 해당 코드의 product tilesdef.xml에서 definition 태그의 name 명칭과 동일

@RequestMapping("/product.do")
	public String viewProduct() {
		return "product";
	}

전체 코드

package kr.spring.tiles.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TilesController {
	@RequestMapping("/")
	public String init() {
		return "redirect:/main.do";
	}
	
	@RequestMapping("/main.do")
	public String viewMain() {
		return "main";
	}
	@RequestMapping("/company.do")
	public String viewCompany() {
		return "company";
	}
	@RequestMapping("/product.do")
	public String viewProduct() {
		return "product";
	}
}




Ch14 Spring Thymeleaf

Setting 하기

pom.xml

버전(2.7.17) 변경, mybatis-spring-boot-starter(2.3.1) 버전 변경, mybatis-spring-boot-starter-test(2.3.1) 버전 변경
pom.xml에서 오류가 발생한다면 maven update 실행

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.7.17</version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.3.1</version>
</dependency>

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter-test</artifactId>
	<version>2.3.1</version>
	<scope>test</scope>
</dependency>

application.yml

jsp는 더 이상 사용하지 않기 때문에 삭제해주고 타임리프 설정을 추가해주면 된다.

spring: 
  profiles: #실행환경(local(로컬),dev(개발),real(운영))
    active: local

  datasource: #DB 접속
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@localhost:1521:xe
    username: user01
    password: 1234
    hikari: #기본 커넥션풀 
      connection-timeout : 30000 #클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간을 설정 30초
      maximum-pool-size : 10 #최대 커넥션 개수 설정
      max-lifetime: 1800000  #커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간 1,800초(30분)
      
  messages: #메시지(파일이 여러개일 경우 ,로 구분) (에러 메세지 처리하는 곳)
    basename: messages.validation
    encoding: UTF-8
    
  devtools: #View reload (클래스, jsp 자동으로 reload 되도록 설정 바로 바로 적용이 되도록)
    livereload:
      enabled: true
    
    restart: #컨트롤러, 모델단이 바뀌었을 때 프로젝트 재시작 설정 (정확히는 classpath(src)에 있는 모든 파일)
      enabled: true
      
  thymeleaf: #타임리프 설정
    prefix: classpath:templates/
    suffix: .html
    check-template-location: true
    enabled: true
    mode: HTML5
    cache: false #캐시는 사용하지 않음(새로고침시 변경사항 바로 반영)

mybatis: #mybatis 설정
  type-aliases-package: kr.spring.**.vo # 폴더가 변경이 가능하다는 의미
  mapper-locations: kr/spring/**/dao/*.xml # dao에 mapper를 위치하겠다
  #mapper-locations: mybatis/mapper/**/*.xml 
  
logging: #로그 지정
  level:
    #root: error
    '[kr.spring]': debug 
    
server:
  port: 8000
  servlet:
    #context-path: /mybatis
    encoding:
      charset: UTF-8
      enabled: true #http 인코딩 지원을 활성화할지 여부
      force: true

kr.spring.board.vo

Board VO

package kr.spring.board.vo;

import java.sql.Date;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;


public class BoardVO {
	private int num;
	@NotBlank
	private String title;
	@NotBlank
	private String writer;
	@NotEmpty
	private String content;
	@NotBlank
	private String passwd;
	
	private Date reg_date;

	public int getNum() {
		return num;
	}

	public void setNum(int num) {
		this.num = num;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getWriter() {
		return writer;
	}

	public void setWriter(String writer) {
		this.writer = writer;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}

	public String getPasswd() {
		return passwd;
	}

	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}

	public Date getReg_date() {
		return reg_date;
	}

	public void setReg_date(Date reg_date) {
		this.reg_date = reg_date;
	}

	@Override
	public String toString() {
		return "BoardVO [num=" + num + ", title=" + title + ", writer=" + writer + ", content=" + content + ", passwd="
				+ passwd + ", reg_date=" + reg_date + "]";
	}
}

kr.spring.board.dao

Board Mapper

package kr.spring.board.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import kr.spring.board.vo.BoardVO;

@Mapper
public interface BoardMapper {

	@Insert("INSERT INTO aboard VALUES(aboard_seq.nextval,#{writer},#{title},#{passwd},#{content},SYSDATE)")
	public void insertBoard(BoardVO board);

	@Select("SELECT COUNT(*) FROM aboard")
	public int getBoardCount();

	public List<BoardVO> getBoardList(Map<String, Integer> map);

	@Select("SELECT * FROM aboard WHERE num=#{num}")
	public BoardVO selectBoard(int num);

	@Update("UPDATE aboard SET writer=#{writer}, title=#{title}, content=#{content} WHERE num=#{num}")
	public void updateBoard(BoardVO board);

	@Delete("DELETE FROM aboard WHERE num=#{num}")
	public void deleteBoard(int num);

}

Board Mapper. xml

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper   
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"   
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  

<!-- interface명칭과 xml의 명칭이 동일해야 한다 -->
<mapper namespace="kr.spring.board.dao.BoardMapper">
	<!-- 목록 가져오기 , 아이디는 따로 명시하지 않았음 그러나 메소드 명을 아이디로 사용 가능함-->
 	<select id="getBoardList" parameterType="map" resultType="boardVO">
		SELECT * FROM(SELECT a.*,rownum rnum FROM (SELECT * FROM aboard ORDER BY num DESC)a) 
		<![CDATA[
		WHERE rnum >= #{start} AND rnum <= #{end}
		]]>
	</select>
	
 </mapper>

kr.spring.board.service

Board Service

package kr.spring.board.service;

import java.util.List;
import java.util.Map;

import kr.spring.board.vo.BoardVO;

public interface BoardService {
	public void insertBoard(BoardVO board);
	public int getBoardCount();
	public List<BoardVO> getBoardList(Map<String, Integer> map);
	public BoardVO selectBoard(int num);
	public void updateBoard(BoardVO board);
	public void deleteBoard(int num);
}

Board Service Impl

package kr.spring.board.service;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import kr.spring.board.dao.BoardMapper;
import kr.spring.board.vo.BoardVO;

@Service
@Transactional
public class BoardServiceImpl implements BoardService{

	@Autowired
	private BoardMapper boardMapper;
	
	@Override
	public void insertBoard(BoardVO board) {
		boardMapper.insertBoard(board);
	}
	
	@Override
	public int getBoardCount() {
		return boardMapper.getBoardCount();
	}

	@Override
	public List<BoardVO> getBoardList(Map<String, Integer> map) {
		return boardMapper.getBoardList(map);
	}

	@Override
	public BoardVO selectBoard(int num) {
		return boardMapper.selectBoard(num);
	}

	@Override
	public void updateBoard(BoardVO board) {
		boardMapper.updateBoard(board);
	}

	@Override
	public void deleteBoard(int num) {
		boardMapper.deleteBoard(num);
	}

	

}

resources > templates > views

selectList.html

타임리프는 태그를 보여줄 것인가 안 보여줄 것인가로 나타냄
jstl은 태그를 지원, 타임리프는 속성을 지원한다고 보면 된다.

타임리프 속성을 이용해서 css 가져오기 th: 접두사
@{/} => ${pageContext.request.contextPath} 같은 의미를 나타냄

<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">

JSTL c:forEach 대신 타임리프 th:each 사용
board 변수를 할당하고 list를 가져오는 것

 <tr th:each="board : ${list}">

th:utext는 타임리프에서 HTML 콘텐츠를 렌더링할 때 사용한다.

<div class="align-center" th:utext="${page}"></div>

th:href 링크의 href 속성을 동적으로 설정
th:if 특정 조건에 따라 HTML 요소를 렌더링할지 여부를 결정
th:each 컬렉션을 반복하여 HTML 요소를 렌더링
th:text 요소의 텍스트 내용을 동적으로 설정
th:utext 텍스트 내용을 HTML로 해석하여 렌더링
@{/} 경로 앞에 슬래시(/)가 포함된 경우, 이는 컨텍스트 루트(context root)에서부터 경로를 지정하는 것을 의미함
@{} 슬래시(/) 없이 단순히 변수나 경로를 지정할 때 사용함

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>게시판 목록</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
    <h2>게시판 목록</h2>
    <div class="align-right">
        <input type="button" value="글쓰기" onclick="location.href='insert.do'">
    </div>
    <div class="result-display" th:if="${count == 0}">표시할 내용이 없습니다.</div>
    <div th:if="${count > 0}">
    <table>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성자</th>
            <th>작성일</th>
        </tr>
        <tr th:each="board : ${list}">
            <td th:text="${board.num}"></td>
            <td><a th:href="@{detail.do(num=${board.num})}" th:text="${board.title}"></a></td>
            <td th:text="${board.writer}"></td>
            <td th:text="${board.reg_date}"></td>
        </tr>
    </table>
    </div>
    <div class="align-center" th:utext="${page}"></div>
</div>
</body>
</html>

insertForm.html

th:object 는 request에 저장되어 있는 자바빈을 호출한다.
th:field 는 자바빈의 필드를 지정한다

th:for 속성은 해당 label 태그가 어떤 input 필드를 참조하는지 명시
*{writer} 는 Thymeleaf의 표현식으로, writer 필드를 의미
th:field="*{writer}" 는 input 필드의 값을 writer 필드와 바인딩하도록 한다. 이를 통해 폼의 데이터와 객체의 필드를 연결할 수 있다.
th:errorclass="field-error" 는 해당 input 필드에 유효성 검사 에러가 발생했을 때 추가할 클래스를 지정합니다. 여기서는 field-error 클래스를 추가하게 됩니다.
th:errors="*{writer}" 는 유효성 검사 에러 메시지를 출력하는 Thymeleaf의 기능

<label th:for="*{writer}">작성자</label>
   <input type="text" th:field="*{writer}" th:errorclass="field-error">
   <span th:errors="*{writer}" class="error-color"></span>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>글 작성하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
    <h2>글쓰기</h2>
    <form action="insert.do" method="post" th:object="${boardVO}">
        <ul>
            <li>
                <label th:for="*{writer}">작성자</label>
                <input type="text" th:field="*{writer}" th:errorclass="field-error">
                <span th:errors="*{writer}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{title}">제목</label>
                <input type="text" th:field="*{title}" th:errorclass="field-error">
                <span th:errors="*{title}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{passwd}">비밀번호</label>
                <input type="password" th:field="*{passwd}" th:errorclass="field-error">
                <span th:errors="*{passwd}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{content}">내용</label>
                <textarea th:field="*{content}" th:errorclass="field-error"></textarea>
                <span th:errors="*{content}" class="error-color"></span>
            </li>
        </ul>
        <div class="align-center">
            <input type="submit" value="등록">
            <input type="button" value="목록" onclick="location.href='list.do'">
        </div>
    </form>
</div>
</body>
</html>

selectDetail.html

글 번호 : [[${board.num}]]
작성자 : [[${board.writer}]]
작성일 : [[${board.reg_date}]]
게시글의 번호, 작성자, 작성일을 출력한다.
[[...]]Thymeleaf의 텍스트 선언 문법으로, 데이터 바인딩을 의미한다.
${board.num}, ${board.writer}, ${board.reg_date}는 각각 게시글 객체의 필드를 참조한다.

th:text="${board.content}" 는 게시글 객체의 content 필드를 출력한다.

<p>
	<span th:text="'글번호 : ' + ${board.num}"></span>
</p>

이 코드도 문제 없이 작성이 되기는 하지만 이렇게 작성하게 되면 더 복잡하게 느껴지기 때문에 [[...]]형태를 사용해준다.

<input type="button" value="수정" th:onclick="|location.href='@{update.do(num=${board.num})}'|" />
<input type="button" value="삭제" th:onclick="|location.href='@{delete.do(num=${board.num})}'|" />

th:onclick="|location.href='@{update.do(num=${board.num})}'|"
th:onclick="|location.href='@{delete.do(num=${board.num})}'|"
이 코드는 Thymeleaf의 텍스트 리터럴(|...|) 문법을 사용하여 버튼이 클릭되었을 때 JavaScript의 location.href를 설정

@{update.do(num=${board.num})}
@{delete.do(num=${board.num})}
Thymeleaf의 URL 표현식으로,
/update.do라는 URL에 num 파라미터를 포함하여 생성
/delete.do라는 URL에 num 파라미터를 포함하여 생성
${board.num}Thymeleaf 표현식으로 게시글 번호를 가져오는 부분

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>글 세부 정보</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
	<div class="page-main">
		<h2 th:text="${board.title}"></h2>
		<p>
			글 번호 : [[${board.num}]] <br> 
			작성자 : [[${board.writer}]] <br>
			작성일 : [[${board.reg_date}]] <br>
		</p>
		<hr width="100%" size="1" noshade="noshade">
		<p th:text="${board.content}"></p>
		<div class="align-center">
			<input type="button" value="수정" th:onclick="|location.href='@{update.do(num=${board.num})}'|" />
			<input type="button" value="삭제" th:onclick="|location.href='@{delete.do(num=${board.num})}'|" />
			<input type="button" value="목록" onclick="location.href='/list.do'">

		</div>
	</div>
</body>
</html>

updateForm.html

th:object="${boardVO}"
폼의 데이터를 바인딩할 객체를 지정한다. Thymeleaf의 표현식으로, 컨트롤러에서 전달받은 게시글 객체를 의미

th:field="*{num}"
<input type="hidden" th:field="*{num}">
숨은 입력 필드로, num 필드를 표시한다. 이 필드는 수정할 게시글의 번호를 서버에 전달하기 위해 사용

입력받는 내용들의 코드는 insertForm과 동일하기 때문에 추가적인 설명은 생략

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시글 수정하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
	<h2>글쓰기</h2>
	<form action="update.do" method="post" th:object="${boardVO}">
		<input type="hidden" th:field="*{num}">
		<ul>
			 <li>
                <label th:for="*{writer}">작성자</label>
                <input type="text" th:field="*{writer}" th:errorclass="field-error">
                <span th:errors="*{writer}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{title}">제목</label>
                <input type="text" th:field="*{title}" th:errorclass="field-error">
                <span th:errors="*{title}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{passwd}">비밀번호</label>
                <input type="password" th:field="*{passwd}" th:errorclass="field-error">
                <span th:errors="*{passwd}" class="error-color"></span>
            </li>
            <li>
                <label th:for="*{content}">내용</label>
                <textarea th:field="*{content}" th:errorclass="field-error"></textarea>
                <span th:errors="*{content}" class="error-color"></span>
            </li>
		</ul>
		<div class="align-center">
			<input type="submit" value="수정">
			<input type="button" value="목록" onclick="location.href='list.do'">
		</div>
	</form>
</div>
</body>
</html>

deleteForm.html

th:field="*{num}"
<input type="hidden" th:field="*{num}">
숨은 입력 필드로, num 필드를 표시한다. 이 필드는 삭제할 게시글의 번호를 서버에 전달하기 위해 사용

비밀번호를 입력하는 것도 insertForm과 동일하기 때문에 추가적인 설명은 생략

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시글 삭제하기</title>
<link rel="stylesheet" th:href="@{/css/style.css}" type="text/css">
</head>
<body>
<div class="page-main">
	<h2>글 삭제</h2>
	<form action="delete.do" method="post" th:object="${boardVO}">
		<input type="hidden" th:field="*{num}">
		<ul>
			 <li>
                <label th:for="*{passwd}">비밀번호</label>
                <input type="password" th:field="*{passwd}" th:errorclass="field-error">
                <span th:errors="*{passwd}" class="error-color"></span>
            </li>
		</ul>
		<div class="align-center">
			<input type="submit" value="삭제">
			<input type="button" value="목록" onclick="location.href='list.do'">
		</div>
	</form>
</div>	
</body>
</html>

게시판 목록

글 작성하기 유효성 체크

글 작성하기

글 작성 완료 후 글 세부 보기

글 수정하기 유효성 체크

글 수정하기 비밀번호 일치 여부 체크

글 수정하기

글 수정 후 글 세부 보기

글 삭제 비밀번호 유효성 체크

글 삭제 비밀번호 일치 여부 체크

글 삭제 후 글 목록

profile
Lucky Things🍀

0개의 댓글