๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Windows/FTP

[JAVA] FTP Util Video Streaming Refactoring - java.lang.IllegalStateException: getOutputStream() has already been called for this response

by ๋ฆฌ์š”_ 2024. 8. 14.
๋ฐ˜์‘ํ˜•

FTP Server Video Streaming

๐ŸŒŸ INDEX


FTP ์„œ๋ฒ„ ์˜์ƒ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฐœ์„ 

FTP ์„œ๋ฒ„ ์˜์ƒ ์ŠคํŠธ๋ฆฌ๋ฐ์ด ์ž˜ ๋˜๋Š” ๊ฒƒ ๊นŒ์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์™„๋ฃŒํ•˜๊ณ  ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ์ค‘.. 
getOutputStream() has already been called for this response ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ „์ฒด ์ ์ธ FTP ์„œ๋ฒ„ ์ƒ์„ฑ ๋ฐ ์„ค์ •, FTP ์™€ ์—ฐ๊ฒฐํ•˜๋Š” API ๋‚ด์šฉ์€ ๋ฒจ๋กœ๊ทธ์—์„œ ์ž‘์„ฑ ํ•ด๋‘์—ˆ์œผ๋‚˜ ์ถ”ํ›„์— ํ‹ฐ์Šคํ† ๋ฆฌ๋กœ ์˜ฎ๊ธธ ์˜ˆ์ •์ด๋‹ค.. ใ…Žใ…Ž

์˜ค๋ฅ˜ ๋‚ด์šฉ

liyo-ftp | 2024-08-14T15:11:06.615+09:00 ERROR 1 --- [service-ftp] [nio-8811-exec-3] [] o.a.c.c.C.[.[.[/].[dispatcherServlet]   
: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: getOutputStream() has already been called for this response] with root cause
liyo-ftp | 
liyo-ftp | java.lang.IllegalStateException: getOutputStream() has already been called for this response
liyo-ftp |       at org.apache.catalina.connector.Response.getWriter(Response.java:549)
liyo-ftp |       at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:188)
liyo-ftp |       at jakarta.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:108)
liyo-ftp |       at jakarta.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:108)
liyo-ftp |       at jakarta.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:108)

getOutputStream() has already been called for this response

์ด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” getOutputStream() ๋ฉ”์„œ๋“œ๊ฐ€ ์ด๋ฏธ ํ˜ธ์ถœ๋œ ํ›„์— getWriter() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ ค๊ณ  ํ•ด์„œ ๋ฐœ์ƒํ•˜๋Š” IllegalStateException์ž…๋‹ˆ๋‹ค.

Java ์„œ๋ธ”๋ฆฟ์—์„œ HTTP ์‘๋‹ต์„ ์ž‘์„ฑํ•  ๋•Œ, ์‘๋‹ต์˜ ๋ฐ”๋””๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ๋ฐฉ๋ฒ•

  • getOutputStream(): ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • getWriter(): ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ด ๋‘ ๋ฉ”์„œ๋“œ๋Š” ๋™์‹œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฉฐ,  getOutputStream()์„ ํ˜ธ์ถœํ•˜์—ฌ ์‘๋‹ต ์ŠคํŠธ๋ฆผ์„ ์—ด์—ˆ์œผ๋ฉด ๊ทธ ํ›„์—๋Š” getWriter()๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†๊ณ , ๊ทธ ๋ฐ˜๋Œ€๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

 

ํ•ด๋‹น ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” getOutputStream() ์ด ์ด๋ฏธ ํ˜ธ์ถœ ๋˜์–ด ์žˆ๋Š”๋ฐ, getWriter()๋ฅผ ํ˜ธ์ถœํ•˜๋ ค ํ•ด์„œ, IllegalStateException ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ•œ ๋ฒˆ์€ ๋™์ž‘ ํ–ˆ์ง€๋งŒ ๋‘ ๋ฒˆ์€ ์•ˆ ๋๋˜ ์ด์œ ... 


์˜ค๋ฅ˜ ๋ถ„์„

/**
* ๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ
* @author liyo
* @param vo
* @param resp
* @throws IOException
*/
public void video(FtpVo vo, HttpServletResponse resp) throws IOException {
	String fileNm = vo.getFileNm();
	FTPClient ftpClient = pool.getFTPClient();
    
	try {
		// ftpClient.enterLocalActiveMode(); // ๋Šฅ๋™
		ftpClient.enterLocalPassiveMode(); // ์ˆ˜๋™
        
		// ํ™•์žฅ์ž๋ณ„ MIME ํƒ€์ž… ์„ค์ •
		String mimeType = getMimeType(fileNm);

		resp.setContentType(mimeType);
		resp.setHeader("Content-Transfer-Encoding", "binary");
		resp.setHeader("Pragma", "no-cache");
		resp.setHeader("Expires", "0");
		resp.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileNm, "UTF-8") + ";");

		OutputStream out = resp.getOutputStream();
		InputStream inputStream = null;

		if (vo.isGroup()) {
			inputStream = ftpClient.retrieveFileStream(fileNm);
		} else {
			inputStream = ftpClient.retrieveFileStream(BesysConstants.NOGROUP + BesysConstants.SLASH + fileNm);
		}

		if (inputStream == null) {
			throw new IOException(ftpClient.getReplyString());
		}

		int len;
		byte[] buf = new byte[8192];
		while ((len = inputStream.read(buf)) != -1) {
			out.write(buf, 0, len);
		}

		out.flush();
		inputStream.close();
		out.close();

		if (!ftpClient.completePendingCommand()) {
			throw new IOException("Could not complete FTP command");
		}

	} catch (Exception e) {
		log.error("Video streaming failed: {}", e);
		throw new IOException("Video streaming failed", e);
	} finally {
		if (ftpClient != null && ftpClient.isConnected()) {
			try {
				ftpClient.logout();
			} catch (IOException ex) {
				log.error("Error logging out from FTP server: {}", ex);
			}
			pool.returnFTPClient(ftpClient);
		}
	}
}

 

์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ์ฝ”๋“œ๋Š” ์œ„์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

1. out.close() ํ˜ธ์ถœ ์‹œ์ 

  • OutputStream์„ ์ง์ ‘ ๋‹ซ๊ธฐ[close()] ์ „์—, ์ŠคํŠธ๋ฆผ์ด ๋‹ซํžŒ ์ƒํƒœ์—์„œ ๋‹ค๋ฅธ ์ŠคํŠธ๋ฆผ์„ ์‹œ๋„ํ•˜๊ฑฐ๋‚˜, ์ด๋ฏธ ์‘๋‹ต์ด ๋๋‚œ ํ›„ ์ถ”๊ฐ€์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ๋„ํ•˜๋Š” ๊ฒฝ์šฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • out.close()๊ฐ€ inputStream.close() ์ดํ›„์— ํ˜ธ์ถœ๋˜๊ณ , ๊ทธ ํ›„์—๋„ ๋” ์ด์ƒ ์‘๋‹ต์— ์ž‘์„ฑ๋˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

2. ์ค‘๋ณต๋œ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ

  • ๋ฉ”์„œ๋“œ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ŠคํŠธ๋ฆผ์„ ์ฒ˜๋ฆฌํ•œ ํ›„, ๋‹ค๋ฅธ ๊ณณ์—์„œ ๊ฐ™์€ ์‘๋‹ต์— ๋Œ€ํ•ด ๋˜ ๋‹ค๋ฅธ ์ŠคํŠธ๋ฆผ์„ ์—ด๋ ค๊ณ  ์‹œ๋„ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ฃผ์˜ ๊นŠ๊ฒŒ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์€ ์ฝ”๋“œ ์™ธ๋ถ€์—์„œ ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ ์ดํ›„, ๋™์ผํ•œ ์‘๋‹ต ๊ฐ์ฒด(HttpServletResponse)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ์ฝ”๋“œ์—์„œ getOutputStream() ๋˜๋Š” getWriter()๋ฅผ ํ˜ธ์ถœํ•˜๋ ค๊ณ  ํ•˜๋Š”์ง€ ์—ฌ๋ถ€์ž…๋‹ˆ๋‹ค.

3. ์˜ˆ์™ธ์ฒ˜๋ฆฌ

  • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ŠคํŠธ๋ฆผ์„ ๋‹ซ์ง€ ์•Š๊ณ , ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ๋Š” ๊ฒฝ์šฐ finally ๋ธ”๋ก์—์„œ ๋˜๋Š” ๋‹ค๋ฅธ ํ›„์† ์ฝ”๋“œ์—์„œ ๋˜ ๋‹ค๋ฅธ ์ŠคํŠธ๋ฆผ์„ ์‹œ๋„ํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์ŠคํŠธ๋ฆผ์ด ์ •์ƒ์ ์œผ๋กœ ๋‹ซํžˆ๋„๋ก ์‹ ๊ฒฝ ์จ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ ๊ฐœ์„  ๋ฐ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ

/**
* ๋น„๋””์˜ค ์ŠคํŠธ๋ฆฌ๋ฐ
* @author liyo
* @param vo
* @param resp
* @throws IOException
*/
public void video(FtpVo vo, HttpServletResponse resp) throws IOException {
    String fileNm = vo.getFileNm();
    FTPClient ftpClient = pool.getFTPClient();
    OutputStream out = null;
    InputStream inputStream = null;

    try {
        // FTP ๋ชจ๋“œ ์„ค์ •
        ftpClient.enterLocalPassiveMode();

        // ํ™•์žฅ์ž๋ณ„ MIME ํƒ€์ž… ์„ค์ •
        String mimeType = getMimeType(fileNm);
        resp.setContentType(mimeType);
        resp.setHeader("Content-Transfer-Encoding", "binary");
        resp.setHeader("Pragma", "no-cache");
        resp.setHeader("Expires", "0");
        resp.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileNm, "UTF-8") + ";");

        // ์‘๋‹ต OutputStream๊ณผ FTP InputStream ์ดˆ๊ธฐํ™”
        out = resp.getOutputStream();
        inputStream = vo.isGroup() ? ftpClient.retrieveFileStream(fileNm) 
                                   : ftpClient.retrieveFileStream(BesysConstants.NOGROUP + BesysConstants.SLASH + fileNm);

        if (inputStream == null) {
            throw new IOException(ftpClient.getReplyString());
        }

        // ๋ฐ์ดํ„ฐ ์ „์†ก
        byte[] buf = new byte[8192];
        int len;
        while ((len = inputStream.read(buf)) != -1) {
            out.write(buf, 0, len);
        }
        out.flush();

        // FTP ๋ช…๋ น ์™„๋ฃŒ ํ™•์ธ
        if (!ftpClient.completePendingCommand()) {
            throw new IOException("Could not complete FTP command");
        }

    } catch (Exception e) {
        log.error("Video streaming failed: {}", e);
        throw new IOException("Video streaming failed", e);
    } finally {
        // ์ŠคํŠธ๋ฆผ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ซ๊ธฐ
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException ex) {
                log.error("Error closing input stream: {}", ex);
            }
        }

        if (out != null) {
            try {
                out.close();
            } catch (IOException ex) {
                log.error("Error closing output stream: {}", ex);
            }
        }

        // FTP ์—ฐ๊ฒฐ ํ•ด์ œ ๋ฐ ๋ฐ˜ํ™˜
        if (ftpClient != null && ftpClient.isConnected()) {
            try {
                ftpClient.logout();
            } catch (IOException ex) {
                log.error("Error logging out from FTP server: {}", ex);
            }
            pool.returnFTPClient(ftpClient);
        }
    }
}

์ˆ˜์ • ๋‚ด์šฉ

1. ์ŠคํŠธ๋ฆผ(InputStream ๋ฐ OutputStream)์„ finally ๋ธ”๋ก์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ซ์Šต๋‹ˆ๋‹ค.

2. FTP ์—ฐ๊ฒฐ ํ•ด์ œ ๋ฐ ๋ฐ˜ํ™˜๋„ finally ๋ธ”๋ก์—์„œ ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

3. close() ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ์„ ์ ์ ˆํ•œ ์œ„์น˜์— ๋ฐฐ์น˜ํ•˜์—ฌ, ํ•œ ๋ฒˆ ์ŠคํŠธ๋ฆผ์„ ๋‹ซ์œผ๋ฉด ์ดํ›„์— ๋” ์ด์ƒ ์‘๋‹ต์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜์ • ์™„๋ฃŒ!

์œ„์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•˜์—ฌ getOutputStream() ๋˜๋Š” getWriter()๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ํ˜ธ์ถœํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

ํฌ์ŠคํŠธ๋งจ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์„ฑ๊ณต์ ์œผ๋กœ ์˜์ƒ์ด ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฉ๋‹ˆ๋‹ค.

 

 

ํ•ด๋‹น API ๋ฅผ ์›น์—์„œ ํ˜ธ์ถœํ•ด๋„ ์ž˜๋‚˜ํƒ€๋‚˜๋Š” ๋ชจ์Šต์ž…๋‹ˆ๋‹ค!


๊ฐœ๋ฐœ ํ™˜๊ฒฝ

  • Backend
    • Framework: Spring Boot 3.2.2
    • Language: Java
    • Gateway: Spring Cloud Gateway
  • Database
    • DBMS: MS SQL Server
    • Container: Docker
  • Development Tools
    • IDE: Visual Studio Code (VSCode)
๋ฐ˜์‘ํ˜•

'Windows > FTP' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Wondows] FTP ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ  (1) 2024.09.26