Controller

package kr.or.ddit.controller.noticeboard.web;

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

import javax.inject.Inject;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.View;

import kr.or.ddit.controller.noticeboard.service.INoticeService;
import kr.or.ddit.controller.noticeboard.view.NoticeDownloadView;
import kr.or.ddit.vo.NoticeFileVO;

@Controller
@RequestMapping("/notice")
public class NoticeDownloadController {
	
	
	@Inject
	private INoticeService noticeService;
	
	@RequestMapping(value="/download.do")
	public View noticeDownloadProcess(int fileNo, Model model) {
		NoticeFileVO noticeFileVO = noticeService.noticeDownload(fileNo);
		Map<String, Object> noticeFileMap = new HashMap<String, Object>();
		noticeFileMap.put("fileName", noticeFileVO.getFileName());
		noticeFileMap.put("fileSize", noticeFileVO.getFileSize());
		noticeFileMap.put("fileSavepath", noticeFileVO.getFileSavepath());
		model.addAttribute("noticeFileMap", noticeFileMap);
		
// 리턴되는 noticeDownloadView는 jsp페이지로 존재하는 페이지 Name을 요청하는게 아니라,
// 클래스를 요청하는 것인데 해당 클래스가 스프링에서 제공하는 AbstractView 클래스를 상속받는 클래스인데
// 그 클래스는 AbstractView를 상속받아 renderMergedOutputModel 함수를 재정의할 때 View로 취급될 수 있게 해준다.
		return new NoticeDownloadView();
	}
}

NoticeDownloadView.java

package kr.or.ddit.controller.noticeboard.view;

import java.io.File;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.view.AbstractView;import freemarker.template.utility.StringUtil;

public class NoticeDownloadView extends AbstractView{

	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		Map<String, Object> noticeFileMap = (Map<String, Object>) model.get("noticeFileMap");
		
		File saveFile = new File(noticeFileMap.get("fileSavepath").toString());
		String fileName = (String) noticeFileMap.get("fileName");
		String agent = request.getHeader("User-Agent");

// 이부분은각 브라우저에서 파일이름이 한글일 경우 깨지는 것을 방지하기 위함
		if(StringUtils.containsIgnoreCase(agent, "msie")||
				StringUtils.containsIgnoreCase(agent, "trident")) {	
			fileName = URLEncoder.encode(fileName, "UTF-8");// IE, Chrome
		}else {
			fileName = new String(fileName.getBytes(), "ISO-8859-1");	//firefox, chrome
		}
		
		response.setHeader("content-Disposition", "attachment; filename=\\"" + fileName + "\\"");
		response.setHeader("content-Length", noticeFileMap.get("fileSize").toString());
		
		// try-with-resource
		// () 안에서 사용되는 객체를 finally 구문에서 close하지 않아도 try-catch 구분이 완료가 되면
		// 자동으로 해당 객체가 close된다.
		try(
			OutputStream os = response.getOutputStream();
		){
			FileUtils.copyFile(saveFile, os);
		}
		
	}
}

Service

package kr.or.ddit.controller.noticeboard.service;

import javax.inject.Inject;

import org.apache.commons.io.input.TaggedInputStream;
import org.springframework.stereotype.Service;

import kr.or.ddit.ServiceResult;
import kr.or.ddit.controller.noticeboard.web.TelegramSendController;
import kr.or.ddit.mapper.NoticeMapper;
import kr.or.ddit.vo.NoticeVO;

@Service
public class NoticeServiceImpl implements INoticeService {
	
	@Inject
	private NoticeMapper noticeMapper;
	
	TelegramSendController tst = new TelegramSendController();
	
	@Override
	public ServiceResult insertNotice(NoticeVO noticeVO) {
		ServiceResult result = null;
		int status = noticeMapper.insertNotice(noticeVO);
		if(status > 0) {
			// 성공 했다는 메시지를 텔레그램 BOT API를 이용하여 알림!
			try {
				tst.sendGet("조성희", noticeVO.getBoTitle());
			} catch (Exception e) {
				e.printStackTrace();
			}
			result = ServiceResult.OK;
		}else {
			result = ServiceResult.FAILED;
		}
		return result;
	}

**// 조회수 증가와 상세보기 서비스**
	@Override
	public NoticeVO selectNotice(int boNo) {
		noticeMapper.incrementHit(boNo);  //조회수 증가
		return noticeMapper.selectNotice(boNo);
	}

**// 파일 다운로드 받는 서비스**
	@Override
	public NoticeFileVO noticeDownload(int fileNo) {
		NoticeFileVO noticeFileVO = noticeMapper.noticeDownload(fileNo);
		if(noticeFileVO == null) {
			throw new RuntimeException();
		}
		
		noticeMapper.incrementNoticeDowncount(fileNo); // 다운로드 횟수 증가
		return noticeFileVO;
	}

}

Mapper

package kr.or.ddit.mapper;

import kr.or.ddit.vo.NoticeVO;

public interface NoticeMapper {
	public int insertNotice(NoticeVO noticeVO);
	public int incrementHit(int boNo);
	public NoticeVO selectNotice(int boNo);
	**public NoticeFileVO noticeDownload(int fileNo);
	public void incrementNoticeDowncount(int fileNo);**
}

NoticeMapper (SQL)

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

<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "<https://mybatis.org/dtd/mybatis-3-mapper.dtd>">
 <mapper namespace="kr.or.ddit.mapper.NoticeMapper">
 	
	
<resultMap type="noticeVO" id="noticeMap">
 		<id property="boNo" column="bo_no"/>
 		<result property="boNo" column="bo_no"/>
 		<result property="boTitle" column="bo_title"/>
 		<result property="boContent" column="bo_content"/>
 		<result property="boWriter" column="bo_writer"/>
 		<result property="boDate" column="bo_date"/>
 		<result property="boHit" column="bo_hit"/>
 		
 		<collection property="noticeFileList" resultMap="noticeFileMap"/>
 	</resultMap>
 	
 	<resultMap type="noticeFileVO" id="noticeFileMap">
 		<id property="fileNo" column="file_no"/>
 		<result property="fileNo" column="file_no"/>
 		<result property="fileName" column="file_name"/>
 		<result property="fileSize" column="file_size"/>
 		<result property="fileFancysize" column="file_fancysize"/>
 		<result property="fileMime" column="file_mime"/>
 		<result property="fileSavepath" column="file_savepath"/>
 		<result property="fileDowncount" column="file_downcount"/>
 	</resultMap>

 	<!-- 공지사항 등록하는 쿼리 -->
 	<insert id="insertNotice" parameterType="noticeVO" useGeneratedKeys="true">
 		<selectKey keyProperty="boNo" resultType="int" order="BEFORE">
 			select seq_notice.nextval from dual
 		</selectKey>
 		insert into notice(
 			bo_no, bo_title, bo_content, bo_writer, bo_date
 		)values(
 			#{boNo}, #{boTitle}, #{boContent}, #{boWriter}, sysdate
 		)
 	</insert>
 	
 	<!-- 조회수 증가 쿼리 -->
 	<update id="incrementHit" parameterType="int">
 		update notice
 		set
 			bo_hit = bo_hit + 1
 		where bo_no = #{boNo}
 	</update>
 	
 ****	<!-- 상세보기 쿼리 -->
<select id="selectNotice" parameterType="int" resultMap="noticeMap">
 		select
 			n.bo_no, bo_title, bo_content, bo_writer,
 			to_char(bo_date, 'yy-mm-dd hh24:mi:ss') bo_date, bo_hit,
 			file_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount
 		from notice n left outer join noticefile nf on(n.bo_no = nf.bo_no)
 		where n.bo_no = #{boNo}
 	</select>

**<!-- 파일 다운받기 위한 정보 얻기 -->**
 	<select id="noticeDownload" parameterType="int" resultType="noticeFileVO">
 		select
 			file_no, bo_no, file_name, file_size, file_fancysize, file_mime, file_savepath, file_downcount
 		from noticefile
 		where file_no = #{fileNo}
 	</select>
 	
 	**<!-- 파일 다운 횟수 업데이트 -->**
 	<update id="incrementNoticeDowncount" parameterType="int">
 		update noticefile
 		set
 			file_downcount = file_downcount + 1
 		where file_no = #{fileNo}
 	</update>
 </mapper>

JSP 페이지

<%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="<http://tiles.apache.org/tags-tiles>" prefix="tiles" %>
<%@ taglib uri="<http://java.sun.com/jsp/jstl/core>" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AdminLTE 3 | Simple Tables</title>

<link rel="stylesheet" href="<https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback>">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/plugins/fontawesome-free/css/all.min.css">
<link rel="stylesheet" href="${pageContext.request.contextPath }/resources/dist/css/adminlte.min.css">
<script src="${pageContext.request.contextPath }/resources/plugins/jquery/jquery.min.js"></script>
<script src="${pageContext.request.contextPath }/resources/ckeditor/ckeditor.js"></script>
</head>
<c:if test="${not empty message }">
<script type="text/javascript">
	alert("${message}");
	<c:remove var="message" scope="request"/>
	<c:remove var="message" scope="session"/>
</script>
</c:if>
<body class="hold-transition sidebar-mini">
	<div class="wrapper">
		<!-- header 영역 -->
		<tiles:insertAttribute name="header"/>
		<div class="content-wrapper">
			<!-- content 영역 -->
			<tiles:insertAttribute name="content"/>
		</div>
		<!--  footer 영역 -->
		<tiles:insertAttribute name="footer"/>

		<aside class="control-sidebar control-sidebar-dark">
		</aside>
	</div>

	<script src="${pageContext.request.contextPath }/resources/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
	<script src="${pageContext.request.contextPath }/resources/dist/js/adminlte.min.js"></script>
</body>
</html>