TortoiseSVN 을 사용할 경우.

Tortoise SVN을 사용할 경우 
 
command로 실행하는 방법은 크게 2가지 이다.
 
1.svn.exe를 이용하는 방법
2.TortoiseProc.exe를 이용하는 방법
 
 

1. SVN Command

 

svnadmin dump

   - 저장소 백업

   - svnadmin dump [저장소 폴더명] > [덤프 파일명]

   - ex) svnadmin dump project_sample > dump_project_sanmple_20010131

 

svnadmin load

   - 저장소 복원

   - svnadmin load [저장소 폴더명] < [덤프 파일명]

   - ex) svnadmin create project_sample

           svnadmin load project_sample < dump_project_sanmple_20010131

 

svn checkout

   - svn 서버에서 소스 내려받기

   - svn checkout svn://192.168.2.2/App App

   - svn checkout -r 100 svn://192.168.2.2/App App   (revision 100번 소스 받기)

   - svn checkout -r HEAD svn://192.168.9.128/sample_repos sample_HEAD     (마지막 리비전 받기)

   - 자세한 것은 svn checkout --help 참고

 

svn export

    - 버전관리 정보 없는 깨끗한 사본을 받아옴
    - svn export --help
    - svn export -r 1 svn://192.168.0.12/sample_repos sample_pos
 

svn commit 파일명 or 폴더명 or ./

   - svn commit 했을 때 소스 파일에서 out of date 에러 날 때는 그 사이에 svn에 소스가 업데이트 됬다는 의미이다. 따라서, out of date 에러났을 때는 svn update을 실행해서 동기화를 한 후에 commit을 하면 된다.

   - commit 하기전에는 update를 실행하여 out of date 에러를 막자.

   - 예)

      # svn commit

      # svn commit ./test/test.h

 

svn resolved -R ./
   - 소스 충돌 날 때 이놈으로 풀면 됨.


svn cleanup
   - 소스가 lock 되 있을 경우 이 명령으로 해제 한다. lock 되는 원인은 알아서 찾는다.

 

svn update

   - 서버와 소스 동기화.

      A  Added

      D  Deleted

      U  Updated

      C  Conflict

      G  Merged

      E  Existed

   - commit 하기전에는 소스 충돌을 막기 위해 update를 실행해야 한다.

   - update를 했는데 에러가 났을 경우에는 각각의 revision을 따로 update 해본다. 경험상 한번에 할 경우에 드물게 에러가 나는 경우가 있다.

      #svn update -r 100   --> 100은 revision number

      #svn update -r 101

 

svn log

   - svn user가 commit한 로그 볼 때 사용한다.

      # svn log svn://192.168.9.128/sample_repos     ==> 주의) svn path를 안주면 마지막 로그를 빼먹는다.

        # svn log -l 10   (상위 10개의 로그만 보기)       ==> 주의) svn path를 안주면 마지막 로그를 빼먹는다.

      # svn log -l 10 svn://192.168.9.128/sample_repos

    

   - commit log 수정하기

      # svn propset --revprop -r [리비전번호] svn:log “수정할 메세지”

       참고: http://altistory.net/929

      # TortoiseSVN 사용할 때 show log 선택 후에 로그 패널에서 마우스 오른쪽 버튼 클릭하면 "Edit log message" 나온다.

 

svn mkdir

   repository에 folder 생성한다.

    - # svn mkdir svn://192.168.163.11/TEST_repo/test_folder

 

svn import

   - 처음으로 repository에 파일 추가하기. 

    - 주의할점은 *.a 같은 파일은 추가 안될 수 있으니 --no-ignore 옵션 사용하기.

    - # svn import import_path svn://192.168.163.11/TEST_repo/import_path --no-ignore

 

svn add

   - 새로 만든 파일 추가 하기. import와 똑같이 사용 가능하다.

   - # svn add --no-ignore [path]

 

svn delete (or del, remove)

   - 파일, 폴더 삭제

   - # svn del [FILE_NAME] or [DIR]

   - # svn del svn://192.168.16.11/TEST_repo/test_dir

      --> commit도 동시에 된다. help 참고.

 

svn switch(or sw)

   - 저장소 이름 변경할 때

    # mv sample_repo1 sample_repo2 (su 로그인 후 저장소 이름을 직접 변경)

    # svn switch --relocate svn:/192.168.9.128/sample_repo1 svn:/192.168.9.128/sample_repo2

       --> repository에서 다운받은 곳에서 명령을 실행한다.

 

    "svn: E155007: 작업 사본이 아닙니다.(Not a working copy)" 이 에러는 .svn 없는 곳에서 svn 명령을 실행

     했을 때 발생한다.

    

   - 저장소 IP 변경할 때

    # svn switch --relocate svn:/192.168.30.198/sample_repo1 svn:/192.168.30.122/sample_repo1

 

svn diff

  - 두 revision 비교 또는 file 비교

  - 참고: http://pokute.tistory.com/201

변경된 파일만 출력

svn diff | awk '/^Index/ {print $NF}'

 

두 리비전 비교

# svn diff -r 100:101

 

두 리비전의 파일비교

#svn diff -r 100:101 file_path

 

참고 링크 : http://blog.daum.net/rayolla/251

 

2.거북이 SVN을 이용해서 실행하기

 

ex) TortoiseProc.exe /command:update /path:"경로" /closeonend:0

 

참고링크 : http://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-automation.html

https://steady-record.tistory.com/entry/WebSocket

 

[Spring] WebSocket 기초 예제

Socket? WebSocket?🔎 - Socket Socket는 네트워크 통신을 위한 도구로, 무전기나 전화기와 같이 프로그램 간에 데이터를 주고받을 수 있게 해준다. 이는 특정한 인스턴스가 아닌 통신 규격을 나타낸다.

steady-record.tistory.com

[Spring] WebSocket 기초 예제

2023. 12. 1. 22:01

Socket? WebSocket?🔎

 

- Socket

  • Socket는 네트워크 통신을 위한 도구로, 무전기나 전화기와 같이 프로그램 간에 데이터를 주고받을 수 있게 해준다. 이는 특정한 인스턴스가 아닌 통신 규격을 나타낸다.
  • 프로그래밍 언어들은 이미 Socket을 구현해두어 프로그래머가 손쉽게 네트워크 통신을 구현할 수 있다.
  • 브라우저도 네트워크를 통해 데이터를 주고받을 때 Socket를 사용하여 통신한다. 그러나 최근에는 웹 기술의 발전으로 인해 Socket 사용률이 줄어들었다.

- WebSocket

  • WebSocket은 웹상에서 동작하는 Socket으로, 기존의 웹 통신 방식과는 다르게 양방향 통신을 지원한다.
  • Ajax와 유사한 면이 있지만 Ajax는 단방향 통신에 주로 사용되며, WebSocket은 웹 페이지에서 서버로부터 데이터를 받는 것뿐만 아니라, 웹 페이지에서 서버로 데이터를 보내는 양방향 통신이 가능하다. 이를 통해 다양한 실시간 데이터를 웹 애플리케이션에 적용할 수 있다.
  • WebSocket은 기존의 웹 통신 방식보다 빠르고 효율적이며, 실시간 기능을 제공하기 위해 널리 사용되고 있다.

WebSocket 프로젝트🔎

WebSocket를 이해할 수 있는 프로젝트를 진행한다.

 

- 파일 구성

상위 패키지(폴더) 하위 패키지(폴더) 파일명
src/main/java com.test.controller SocketController.java
  com.test.server SocketServer.java
WEB-INF views test.jsp

 


- 환경 설정

1. 자바 및 스프링 버전 변경 - pom.xml 

2. 의존성 추가

 

WebSocket를 사용하기 위한 의존성을 추가한다.

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-websocket</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
	
		<dependency>
			<groupId>javax.websocket</groupId>
			<artifactId>javax.websocket-api</artifactId>
			<version>1.1</version>
		</dependency>

 


- SocketServer.java

WebSoket 으로 클라이언트와 서버가 통신할 때는 Controller의 역할은 view를 띄어주는 역할만 존재한다. 

package com.test.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SocketController {
	
	@GetMapping(value = "/test.do")
	public String test(Model model) {
		return "test";
	}
}

 


📢 잠깐 여기서, 클라이언트와 SocketServer 의 관계

1. 일방적인 데이터 송수신

웹 소켓을 사용하면 양방향 통신이 가능하며, 클라이언트와 서버 간에 언제든 데이터를 주고 받을 수 있다. 이는 HTTP와는 다르게 일방적인 클라이언트에서 요청하고 서버가 응답하는 구조가 아닌, 양쪽에서 언제든 데이터를 주고 받을 수 있다.

 

2. 클라이언트의 연결 시작

Socketserver는 클라이언트가 누군지 모르기 때문에  웹 소켓에서는 클라이언트가 먼저 연결을 시작해야 한다. 이를 통해 클라이언트는 서버에게 연결을 요청하고, 서버는 해당 연결을 수락하여 양방향 통신이 가능한 상태가 된다.


3. @ServerEndpoint

@ServerEndpoint 어노테이션은 Java에서 웹 소켓 엔드포인트를 정의하는 데 사용된다. 종단점은 웹 소켓 서버에서 클라이언트의 요청을 처리하고, 연결을 관리하는 핵심 구성 요소이다.

 

4. 연결 지속성

한 쪽에서 일방적으로 연결을 끊어버리지 않는 한, 웹 소켓 연결은 계속해서 지속된다. 이 특징은 HTTP와 달리 지속적인 양방향 통신이 가능하도록 한다.


- WebSocket 연결하기

위 2번 특징으로 클라이언트(jsp)에서 먼저 연결 시도를 한다.

화면의 연결 버튼을 누르면 4가지 단계가 이뤄져야한다.

1. 소켓 생성

2. 서버 접속(연결)

3. 통신

4. 서버 접속 해제(종료)

 

test.jsp

test.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocketTest</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
</head>
<body>
	<!-- test.jsp -->
	<h1>WebSocket <small>연결 테스트</small></h1>
	
	<div>
		<button type="button" class="in" id="btnConnect">연결하기</button>
		<button type="button" class="out" id="btnDisConnect">종료하기</button>
	</div>
	<hr>
	
	<div>
		<input type="text" class="long" id="msg">
		<button type="button" id="btnMsg">보내기</button>
	</div>
	
	<div class="message full"></div>
	<script src="https://code.jquery.com/jquery-1.12.4.js" ></script>
</body>
</html>

 

 

test.jsp (JavaScript)

	<script>
		const url = 'ws://localhost:8090/socket/testserver.do';
		
		let ws; //웹 소켓 참조 변수
        
		$('#btnConnect').click(function() {
			
			ws = new WebSocket(url);
			
			ws.onopen = function(evt) {
				log('서버와 연결하였습니다.');
			};
			
			ws.onclose = function(evt) {
				log('서버와 연결이 종료되었습니다.');
			};
			
			ws.onmessage = function(evt) {
				log(evt.data);
			};
			
			ws.onerror = function(evt) {
				log('에러가 발생했습니다.' + evt);
			};
		});
		
		$('#btnDisConnect').click(function() {
			ws.close();
			log('서버와 연결 종료를 시도합니다.');
			
		});
		
		function log(msg) {
			$('.message').prepend(`
				<div>[\${new Date().toLocaleTimeString()}] \${msg}</div>		
			`);
		}
		
		$('#btnMsg').click(function() {
			ws.send($('#msg').val());
			log('메시지를 전송했습니다.');
			
			$('#msg').val('');
		});
		
	</script>

 

 

const url = 'ws://localhost:8090/socket/testserver.do';

클라이언트와 서버 연결 할 때는 'http' 이 아닌 WebSocket 의 약자인 'ws' 프로토콜을 사용한다.

위 주소 중 testserver.do는 서버의 @ServerEndpoint 와 연결하여 클라이언트에서 서버를 매핑하는 역할을 한다. 

 

ws = new WebSocket(url);

WebSocket 메소드를 사용하면 소켓 생성과 동시에 서버에 접속 시도한다.

 

웹 소켓 이벤트

웹 소켓에는 각종 행위들이 이벤트로 구현되어있어, 클라이언트는 비동기 방식으로 다른 업무를 하다가 이벤트로 연락 오는 것을 확인할 수 있다.

ws.onopen : 서버측에서 소켓 연결을 받아들이고 연결이 되는 순간 이벤트가 발생한다.

ws.onclose : 웹 소켓 연결이 닫힐 때 발생한다.

ws.onmessage : 웹 소켓으로부터 메시지를 수신했을 때 발생한다.

ws.onerror : 웹 소켓 통신 중 에러가 발생했을 때 호출된다.

 

 

ws.send($('#msg').val());

연결된 서버에게 메시지를 전송할 때는 ws.send('전달할 메시지') 를 사용한다.

메시지를 서버에 전송하고나면 다음 입력을 위해 초기화를 한다.

 

SocketServer.java

package com.test.server;

@ServerEndpoint("/testserver.do")
public class SocketServer {

	@OnOpen
	public void handelOpen() {
		System.out.println("클라이언트가 접속했습니다.");
	}
	
	@OnClose
	public void handleClose() {
		System.out.println("클라이언트가 종료했습니다.");
	}
	
	@OnMessage
	public String handleMasseage(String msg) {
		System.out.println("클라이언트가 보낸 메시지: " + msg);
		
		return "(응답)" + msg;
	}
	
	@OnError
	public void handleError(Throwable e) {
		System.out.println("에러 발생 " + e.getMessage());
	}
	
}

 

@ServerEndpoint("/testserver.do")

위에서 클라이언트가 testserver.do 를 매핑하였다.

 

@OnMessage

서버 측에서 웹 소켓 이벤트를 처리하기 위해 어노테이션을 사용한다.

이 어노테이션은 클라이언트가 서버에게 메시지를 전송했을 때(ws.send) 반응하는 이벤트이다.  전달한 메시지가 매개변수로 설정된다.

 

보통 메소드 이름은 'handle + 이벤트' 으로 작명한다.

 

연결 및 메시지 전송 모습

 

연결, 전송, 연결종료
공유하기
게시글 관리
구독하기
 

Socket? WebSocket?🔎

 

- Socket

  • Socket는 네트워크 통신을 위한 도구로, 무전기나 전화기와 같이 프로그램 간에 데이터를 주고받을 수 있게 해준다. 이는 특정한 인스턴스가 아닌 통신 규격을 나타낸다.
  • 프로그래밍 언어들은 이미 Socket을 구현해두어 프로그래머가 손쉽게 네트워크 통신을 구현할 수 있다.
  • 브라우저도 네트워크를 통해 데이터를 주고받을 때 Socket를 사용하여 통신한다. 그러나 최근에는 웹 기술의 발전으로 인해 Socket 사용률이 줄어들었다.

- WebSocket

  • WebSocket은 웹상에서 동작하는 Socket으로, 기존의 웹 통신 방식과는 다르게 양방향 통신을 지원한다.
  • Ajax와 유사한 면이 있지만 Ajax는 단방향 통신에 주로 사용되며, WebSocket은 웹 페이지에서 서버로부터 데이터를 받는 것뿐만 아니라, 웹 페이지에서 서버로 데이터를 보내는 양방향 통신이 가능하다. 이를 통해 다양한 실시간 데이터를 웹 애플리케이션에 적용할 수 있다.
  • WebSocket은 기존의 웹 통신 방식보다 빠르고 효율적이며, 실시간 기능을 제공하기 위해 널리 사용되고 있다.

WebSocket 프로젝트🔎

WebSocket를 이해할 수 있는 프로젝트를 진행한다.

 

- 파일 구성

상위 패키지(폴더) 하위 패키지(폴더) 파일명
src/main/java com.test.controller SocketController.java
  com.test.server SocketServer.java
WEB-INF views test.jsp

 


- 환경 설정

1. 자바 및 스프링 버전 변경 - pom.xml 

2. 의존성 추가

 

WebSocket를 사용하기 위한 의존성을 추가한다.

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-websocket</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
	
		<dependency>
			<groupId>javax.websocket</groupId>
			<artifactId>javax.websocket-api</artifactId>
			<version>1.1</version>
		</dependency>

 


- SocketServer.java

WebSoket 으로 클라이언트와 서버가 통신할 때는 Controller의 역할은 view를 띄어주는 역할만 존재한다. 

package com.test.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SocketController {
	
	@GetMapping(value = "/test.do")
	public String test(Model model) {
		return "test";
	}
}

 


📢 잠깐 여기서, 클라이언트와 SocketServer 의 관계

1. 일방적인 데이터 송수신

웹 소켓을 사용하면 양방향 통신이 가능하며, 클라이언트와 서버 간에 언제든 데이터를 주고 받을 수 있다. 이는 HTTP와는 다르게 일방적인 클라이언트에서 요청하고 서버가 응답하는 구조가 아닌, 양쪽에서 언제든 데이터를 주고 받을 수 있다.

 

2. 클라이언트의 연결 시작

Socketserver는 클라이언트가 누군지 모르기 때문에  웹 소켓에서는 클라이언트가 먼저 연결을 시작해야 한다. 이를 통해 클라이언트는 서버에게 연결을 요청하고, 서버는 해당 연결을 수락하여 양방향 통신이 가능한 상태가 된다.


3. @ServerEndpoint

@ServerEndpoint 어노테이션은 Java에서 웹 소켓 엔드포인트를 정의하는 데 사용된다. 종단점은 웹 소켓 서버에서 클라이언트의 요청을 처리하고, 연결을 관리하는 핵심 구성 요소이다.

 

4. 연결 지속성

한 쪽에서 일방적으로 연결을 끊어버리지 않는 한, 웹 소켓 연결은 계속해서 지속된다. 이 특징은 HTTP와 달리 지속적인 양방향 통신이 가능하도록 한다.


- WebSocket 연결하기

위 2번 특징으로 클라이언트(jsp)에서 먼저 연결 시도를 한다.

화면의 연결 버튼을 누르면 4가지 단계가 이뤄져야한다.

1. 소켓 생성

2. 서버 접속(연결)

3. 통신

4. 서버 접속 해제(종료)

 

test.jsp

test.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocketTest</title>
<link rel="stylesheet" href="https://me2.do/5BvBFJ57">
</head>
<body>
	<!-- test.jsp -->
	<h1>WebSocket <small>연결 테스트</small></h1>
	
	<div>
		<button type="button" class="in" id="btnConnect">연결하기</button>
		<button type="button" class="out" id="btnDisConnect">종료하기</button>
	</div>
	<hr>
	
	<div>
		<input type="text" class="long" id="msg">
		<button type="button" id="btnMsg">보내기</button>
	</div>
	
	<div class="message full"></div>
	<script src="https://code.jquery.com/jquery-1.12.4.js" ></script>
</body>
</html>

 

 

test.jsp (JavaScript)

	<script>
		const url = 'ws://localhost:8090/socket/testserver.do';
		
		let ws; //웹 소켓 참조 변수
        
		$('#btnConnect').click(function() {
			
			ws = new WebSocket(url);
			
			ws.onopen = function(evt) {
				log('서버와 연결하였습니다.');
			};
			
			ws.onclose = function(evt) {
				log('서버와 연결이 종료되었습니다.');
			};
			
			ws.onmessage = function(evt) {
				log(evt.data);
			};
			
			ws.onerror = function(evt) {
				log('에러가 발생했습니다.' + evt);
			};
		});
		
		$('#btnDisConnect').click(function() {
			ws.close();
			log('서버와 연결 종료를 시도합니다.');
			
		});
		
		function log(msg) {
			$('.message').prepend(`
				<div>[\${new Date().toLocaleTimeString()}] \${msg}</div>		
			`);
		}
		
		$('#btnMsg').click(function() {
			ws.send($('#msg').val());
			log('메시지를 전송했습니다.');
			
			$('#msg').val('');
		});
		
	</script>

 

 

const url = 'ws://localhost:8090/socket/testserver.do';

클라이언트와 서버 연결 할 때는 'http' 이 아닌 WebSocket 의 약자인 'ws' 프로토콜을 사용한다.

위 주소 중 testserver.do는 서버의 @ServerEndpoint 와 연결하여 클라이언트에서 서버를 매핑하는 역할을 한다. 

 

ws = new WebSocket(url);

WebSocket 메소드를 사용하면 소켓 생성과 동시에 서버에 접속 시도한다.

 

웹 소켓 이벤트

웹 소켓에는 각종 행위들이 이벤트로 구현되어있어, 클라이언트는 비동기 방식으로 다른 업무를 하다가 이벤트로 연락 오는 것을 확인할 수 있다.

ws.onopen : 서버측에서 소켓 연결을 받아들이고 연결이 되는 순간 이벤트가 발생한다.

ws.onclose : 웹 소켓 연결이 닫힐 때 발생한다.

ws.onmessage : 웹 소켓으로부터 메시지를 수신했을 때 발생한다.

ws.onerror : 웹 소켓 통신 중 에러가 발생했을 때 호출된다.

 

 

ws.send($('#msg').val());

연결된 서버에게 메시지를 전송할 때는 ws.send('전달할 메시지') 를 사용한다.

메시지를 서버에 전송하고나면 다음 입력을 위해 초기화를 한다.

 

SocketServer.java

package com.test.server;

@ServerEndpoint("/testserver.do")
public class SocketServer {

	@OnOpen
	public void handelOpen() {
		System.out.println("클라이언트가 접속했습니다.");
	}
	
	@OnClose
	public void handleClose() {
		System.out.println("클라이언트가 종료했습니다.");
	}
	
	@OnMessage
	public String handleMasseage(String msg) {
		System.out.println("클라이언트가 보낸 메시지: " + msg);
		
		return "(응답)" + msg;
	}
	
	@OnError
	public void handleError(Throwable e) {
		System.out.println("에러 발생 " + e.getMessage());
	}
	
}

 

@ServerEndpoint("/testserver.do")

위에서 클라이언트가 testserver.do 를 매핑하였다.

 

@OnMessage

서버 측에서 웹 소켓 이벤트를 처리하기 위해 어노테이션을 사용한다.

이 어노테이션은 클라이언트가 서버에게 메시지를 전송했을 때(ws.send) 반응하는 이벤트이다.  전달한 메시지가 매개변수로 설정된다.

 

보통 메소드 이름은 'handle + 이벤트' 으로 작명한다.

 

연결 및 메시지 전송 모습

 

연결, 전송, 연결종료

우선 Eclipse나 STS는 정적파일 같은 경우에는 바로 자동반영이 되지만 서버단 JAVA코드같은 경우에는 jLebel이라는 유료 플러그인을 사용하여야한다.

IntelliJ에서도 정적파일 같은 경우에는 설정을통해 자동으로 반영할 수 있다.

(서버단 코드는 반영되지 않음. 이클립스에서 java파일 저장시 서버가 새롭게 로드되는 불편한? 증상과 비슷한 원리로 로딩됨...)

 

1. gradle.build 파일을 연다.

 

2. dependencies에 아래 코드 를 추가한다.

dependencies {

	...
    
	developmentOnly('org.springframework.boot:spring-boot-devtools')
    //혹은
    //developmentOnly 'org.springframework.boot:spring-boot-devtools'
}

 

 

3. 상단 메뉴바 file탭의 Settings

 

 

4. Settings 창이 열리면

Settings - Bulid,Execution, Deployment - compiler를 찾아 들어가거나,

검색란에 Compiler를 검색한다.

Settings - Bulid,Execution, Deployment - compiler

 

4. 우측화면 체크박스

 

Automatically show first error in editor 체크 

Build Project automatically 체크

 

5. 좌측 메뉴탭 Advanced Settings 선택

 

6. 우측 화면에서 Allow auto-make to start even if developed application is currently running 체크

 

 

7. bulid,Execution, Deployment - Build Tools - Gradle선택 혹은 검색창에 Gradle 검색

 

8. Build and run using 과 Run test using 두개 모두 Gradle로 되어있다면 IntelliJ IDEA로 변경한다.

 

7~8번은 컴파일 실행시 빌드를 Gradle이 아닌 IntelliJ IDEA로 설정하는 것인데, 이것은 자동반영 효과 뿐만 아니라 전반적인 빌드 속도를 향상시켜준다. (분산방식으로 빌드를 해준다나 뭐라나 잘 모르겠다 좀 빨라지긴 한다.)

 

9. 모든 설정 완료후에 확인차 인텔리제이를 껐다 재실행 시키자.

Log에 restartedMain이라는 단어가 출력되면 정상적으로 세팅이 되었다는 뜻

맥 프로그램 손상되었기에 때문에 열수 없습니다.

휴지통으로 이동 합니다.

 

1. sudo spctl --master-disable

안되면

2. xattr -cr "path"

난 2번이 먹음 1번 안됨

단순 List나 Array의 foreach INSERT, UPDATE, DELETE, MERGE 문의 관한 내용은 아래 게시글을 참고하자.

 

 

 

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE, UPDATE) - Oracle

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE, UPDATE) - Oracle foreach문은 사용할 때마다 사용되는 위치도 속성도 어려워서 더이상 헷갈리지 않기 위해 남겨본다 :-) ! MySQL foreach 사용방.....

haenny.tistory.com

 

 

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL Oracle 버전이 궁금하다면 ? 클릭 ! [MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE) [MyBatis] List 파라메터 foreac.....

haenny.tistory.com

 

foreach

collection : 넘어온 파라미터의 반복하기 원하는 파라미터를 입력하여 주면 된다. 예를 들어 vo의 testMap이라는 Map이 있다면 collection에 testMap을 넣어주면 된다.

item : List의 경우 순차적으로 반복하여 값이 저장된다. item을 data라고 하였을 경우 WHERE col = #{data} 이런식으로 사용이 가능하다. Map에서는 key의 value가 저장된다.

separator : 반복 되는 사이에 출력 할 문자열

open : 해당 구문이 시작될때 삽입되는 문자열

close : 해당 구문이 종료될때 삽입되는 문자열

index : List의 경우 index 번호, Map의 경우 key 값이 저장된다.

 

<foreach collection="Map or List or Array" item="alias" ></foreach>

 

Map 안에 Map (or Value Object)

public class TestVO {
	private String name;
	private Map<String, String> infoObj;
}

 

TestVO에 다음과 같은 구조의 데이터가 들어있다고 가정할 때, NAME, AGE와 COLOR 컬럼 데이터를 INSERT 해야한다.

{
	name: "haenny",
	info: {	
		// key : value = age : color    
		// 나이에 좋아했던 색상   
		14 : "pink",
		15 : "yellow",
		20 : "blue"
	}
}

 

 

List 형태의 경우 TestVO를 foreach 돌려서 사용하면되지만, Map의 경우 Key와 Value 값을 가져올 수 있나? List이거나 Array가 아니기 때문에 foreach 문법을 사용할 수 없는 거 아닌가? 생각할 수 있다.

 

<select id="insInfo" parameterType="testVO">
	INSERT INTO TEST_TBL (
		NAME,
		AGE,
		COLOR
	)
	<foreach collection="info" item="value" index="key" separator="" open="" close="">
		SELECT #{name} AS NAME,		// TEST VO에서 바로 가져온 값
			#{key} AS AGE,		// info 라는 collection (Map) 의 Key 값
			#{value} AS COLOR	// info 라는 collection (Map) 의 Value 값    
	  	FROM DUAL     
	</foreach>
</select>

 

INDEX 를 List나 Array의 반복되는 구문 형태로만 사용된다고 생각할 수 있지만, Map의 경우 Key 값이 저장된다.

 

 

Map 안에 List

 

<select id="getTest" parameterType="testVO2" resultType="java.util.list">
	SELECT TEST
	FROM TEST_TBL
	WHERE 
    <foreach collection="mapData" item="value" index="key" separator="AND">
    	#{key} IN
        // mapData = {key : value} 형태인데 value 값이 List 형태인 경우이다
        <foreach collection="value" item="item" index="idx" separator="or"  open="(" close=")">
        	#{item}
        </foreach>
    </foreach>
</select>

 

 

https://haenny.tistory.com/284  에서 퍼옴 정리 잘되어 있네 살아있네 ㅎ

415 Unsupported Media Type

한번쯤 만나봤을 이 골치아픈 415는 클라이언트(View)와 서버(Controller)의 요청/응답하는 데이터의 매개변수 설정이 잘못되었을 때 주로 발생한다.

 

오늘 해결방법으로 두 가지를 모두 살펴볼 것이다.

  1. 헤더 타입 설정
  2. RequestBody 설정

 

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

추천인 코드 : AF8800551

HTTP Request, Content-Type 헤더와 Accept 헤더 확인하기

 

Content-Type 헤더와 Consumes 설정
@RequestMapping의 consumes 설정과 Content-Type request 헤더가 일치할 경우에 URL이 호출된다.

 

Content-Type은 HTTP 메시지(요청과 응답 모두)에 담겨 보내는 데이터 형식을 알려주는 헤더이다.
대부분의 브라우저와 웹서버는 HTTP 표준 스펙을 따르는 Content-Type 헤더를 기준으로 HTTP 메시지에 담긴 데이터를 분석·파싱한다.
그러나 HTTP 요청의 경우 GET방식인 경우는 무조건 URL 끝에 쿼리스트링(key=value) 형식이기 때문에 Content-Type 헤더가 굳이 필요없다.
따라서 Content-Type은 POST방식이나 PUT방식처럼 BODY에 데이터를 싣어 보낼 때 중요하다.

 

일반적으로는 사용되는 Content-Type은 3가지가 있다.

 

application/json

요청 데이터가 Json {key : value}  형식일 때 사용한다.

@RequestMapping(value="/test.do", method=RequestMethod.POST, consumes="application/json;")
contentType : "application/json"

 

 

application/x-www-form-urlencoded

요청 데이터가 쿼리스트링(key=value) 형식일 때 사용한다.

 

HTML 폼을 만들어 데이터를 전송할 때 주로 사용하는 헤더이지만, 요즘은 Json Handling을 많이하기 때문에 사용이 예전에 비해 많이 줄었다.

@RequestMapping(value="/test.do", method=RequestMethod.POST, consumes="application/x-www-form-urlencoded")
contentType : "application/x-www-form-urlencoded"

 

multipart/form-data

요청 데이터에 파일이 첨부될 수 있음을 알릴 때 사용한다.

@RequestMapping(value="/test.do", method=RequestMethod.POST, consumes="multipart/form-data")
contentType : "multipart/form-data"

 

Accept 헤더와 produces 설정
@RequestMapping의 produces 설정과 Accept request 헤더가 일치할 경우에 URL이 호출된다.

브라우저(클라이언트)에서 웹서버로 요청 시 요청 메시지에 담기는 헤더이다.
Accept 헤더는 자신에게 이러한 데이터 타입만 허용하겠다는 뜻으로, 브라우저가 요청 메시지의 Accept 헤더 값을 application/json 이라고 설정했다면
웹 서버에게 나는 json 데이터만 처리할 수 있으니, json 데이터 형식으로 응답을 돌려줘라고 말하는 것과 같다.

@RequestMapping(value="/test.do", method=RequestMethod.POST, produces="application/json;")
dataType : "json"	// ajax

 

 

URI를 호출하는 클라이언트쪽은 Ajax를 기준의 예시로 작성하였다.

만약 XMLHttpRequest의 예시가 궁금하다면 각각 다음처럼 사용할 수 있다.

 

var xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.setRequestHeader('Content-type', 'application/json; charset=UTF-8;');

 

 

데이터 타입별 예제는 아래 게시글에 자세히 설명되어있으니 참고하자.

 

springboot form, ajax 데이터 타입별 Controller에서 받는 방법 @RequestParam, @RequestBody

springboot form, ajax 데이터 타입별 Controller에서 받는 방법 @RequestParam, @RequestBody 이번 포스팅에서는 springboot web project에서 form 과 ajax로 자주 사용되는 데이터타입을 전송하고 받는 방법을.....

aljjabaegi.tistory.com

 

 

 

 

@RequestBody 

위의 헤더 타입을 맞추고 나서도 415 에러가 계속 떨어지는 경우는 RequestBody 설정이 제대로 되어있지 않을 확률이 99%이다 !!

 

Jackson 라이브러리 의존성 추가

pom.xml 위치에 다음 의존성을 추가한다.

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0</version>
</dependency>

 

annotation-driven 추가 ★

dispatchar-servlet.xml (WEB-INF/config/springmvc 경로)에 다음 태그를 추가한다.

<mvc:annotation-driven />

 

Spring 3.0부터 제공하고 있는 annotation 기반의 controller 호출이나 bean  객체 등록, 매핑의 작업을 편리하게 해준다.

<mvc:annotation-driven/>을 사용하면 내부에서 자동으로 RequestMappingHandlerMapping 과 RequestMappingHandlerAdapter를 구성해주어 Handler 역할을 쉽게 구현시켜주는 것이다.

 

이 설정을 놓치는 부분이 생각보다 굉장히 많다. (나포함)

설정을 하지 않고 RequestBody를 사용한다면 제대로 동작하지 않을 수 있다.

 

https://haenny.tistory.com/280  에서 퍼옴 정리 잘되어 있음

OUTER(외부) JOIN 이란 조인 조건에서 동일한 값이 없는 행도 반환할 때 사용하는 구문이다.
즉 A, B 테이블을 JOIN 할 경우, 조건에 맞지 않는 데이터도 표시하고 싶을 때 OUTER JOIN을 사용한다.

 

오늘은 LEFT OUTER JOIN, RIGHT OUTER JOIN, FULL OUTER JOIN을 살펴볼 것이다 !!

 

LEFT / RIGHT / OUTER JOIN 맛보기

 

 

 

OUTER JOIN은 조인 USING, ON 조건절을 필수적으로 사용해야 한다.

 

LEFT OUTER JOIN과 RIGHT OUTER JOIN은 기준이 되는 테이블이 조인 수행 시 무조건 드라이빙 테이블(첫번째로 ACCESS 되는 테이블)이 된다.

 

또한, 드라이빙 테이블이 어떤 테이블이냐에 따라 쿼리 성능이나 튜닝 부분에서 굉장히 중요한 영향을 끼치기 때문에 데이터 양보다는 무조건 적은 데이터를 추출하는 테이블을 드라이빙 테이블로 잡는 것이 중요하다. 

 

 

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다"

추천인 코드 : AF8800551

 

 

사용 방법

SELECT *                              -- 조회할 컬럼을 선택한다 (모든 컬럼 조회)
  FROM TABLE1  T1                             
  LEFT/RIGHT/FULL OUTER JOIN TABLE2  T2               
  ON (T1.KEY = T2.KEY)                -- 두 테이블을 연결할 조건을 명시한다

 

USING 조건절을 사용하는 경우는 조인하는 테이블 내에 있는 컬럼들이 동일한 이름을 가지고 있을 때 사용 가능하다.

SELECT *                     -- 조회할 컬럼을 선택한다 (모든 컬럼 조회)
  FROM TABLE1  T1                             
  LEFT/RIGHT/FULL OUTER JOIN TABLE2  T2                
  USING (KEY)                -- 두 테이블이 동일하게 갖고 있는 컬럼명으로 조건을 명시한다

예제

아래와 같이 KEY 컬럼이 TIMESTAMP NUM인 같은 구조의 테이블 A와 B가 있다.

 

 

 

LEFT JOIN (LEFT OUTER JOIN)

A 테이블에 LEFT OUTER JOIN B를 하는 경우

 

A,B 테이블의 조인 조건이 맞는 경우 B 테이블의 컬럼에서 해당 데이터를 가져오고, 조인 조건이 맞지 않는 경우 각 테이블의 레코드 중 A 테이블의 레코드는 무조건 결과값에 포함되어 나오고 B테이블에서 가져온 컬럼들은 NULL로 채운다.

SELECT A.TIMESTAMP
     , A.NUM
     , A.VERSION
     , A.MSG_ID
     , A.GRADE
     , B.TIMESTAMP
     , B.NUM
     , B.VERSION
     , B.MSG_ID
     , B.GRADE 
  FROM A
  LEFT JOIN B ON(B.TIMESTAMP = A.TIMESTAMP AND B.NUM = A.NUM)

 

키 값인 TIMESTAMP와 VIN이 매칭되는 데이터를 함께 보여주고 있지만, 왼쪽 테이블(A)를 기준으로 매칭되지 않는 데이터까지 보여주고 있다.

 

 

 RIGHT JOIN (RIGHT OUTER JOIN)

LEFT JOIN과 반대로 우측 테이블, 즉 테이블 B가 기준이 되어 결과를 보여준다.

SELECT A.TIMESTAMP
     , A.NUM
     , A.VERSION
     , A.MSG_ID
     , A.GRADE
     , B.TIMESTAMP
     , B.NUM
     , B.VERSION
     , B.MSG_ID
     , B.GRADE 
  FROM A
  RIGHT JOIN B ON(B.TIMESTAMP = A.TIMESTAMP AND B.NUM = A.NUM)

키 값인 TIMESTAMP와 VIN이 매칭되는 데이터를 함께 보여주고 있지만, 오른쪽 테이블(B)를 기준으로 매칭되지 않는 데이터까지 보여주고 있다.

 

 

 FULL OUTER JOIN

좌측테이블 A와 우측 테이블 B의 데이터를 모두 읽어 중복된 데이터는 삭제한 JOIN 결과를 보여준다. 

SELECT A.TIMESTAMP
     , A.NUM
     , A.VERSION
     , A.MSG_ID
     , A.GRADE
     , B.TIMESTAMP
     , B.NUM
     , B.VERSION
     , B.MSG_ID
     , B.GRADE 
  FROM A
  FULL OUTER JOIN B ON(B.TIMESTAMP = A.TIMESTAMP AND B.NUM = A.NUM)

 

 

키 값인 TIMESTAMP와 VIN이 매칭되는 데이터를 함께 보여주고 있고, 두 테이블 A와 B에서 매칭되지 않는 데이터도 함께 보여주고 있다.

 

 

 

 

 

만약 조인 조건으로 매칭되는 데이터만 보고 싶다면 INNER JOIN을 사용하면 된다.

자세한 내용은 아래 링크를 참고하자.

 

Link : https://haenny.tistory.com/35

 

[Oracle] INNER JOIN이란? 사용방법도 함께 알아보자

[Oracle] INNER JOIN이란? 사용방법도 함께 알아보자 INNER(내부) JOIN 이란, 두 테이블간 조인 조건을 만족하는 행을 반환할 때 사용하는 구문이다. 쉽게 조인하려는 두 테이블의 교집합이라고 생각하면 될 듯 하.....

haenny.tistory.com

 

 

FULL OUTER JOIN 의 같은 예제로 USING 사용 예시

SELECT A.TIMESTAMP
     , A.NUM
     , A.VERSION
     , A.MSG_ID
     , A.GRADE
     , B.TIMESTAMP
     , B.NUM
     , B.VERSION
     , B.MSG_ID
     , B.GRADE 
  FROM A
  FULL OUTER JOIN B USING (TIMESTAMP ,NUM)

 

단, USING 구문은 MS SQL Server와 Sybase에서는 지원하지 않는다고하니 참고하자.

 

 

https://haenny.tistory.com/34  에서 퍼옴 정리 잘됨

리스트 파라메터의 foreach 사용방법에 대해서는 아래 게시글을 참고해주세요.

 

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE)

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE) foreach문은 사용할 때마다 사용되는 위치도 속성도 어려워서 더이상 헷갈리지 않기 위해 남겨본다 :-) ! foreach문의 속성은 다음과 같다. 구분.....

haenny.tistory.com

 

 

시퀀스를 키값으로 갖는 테이블의 데이터를 INSERT할 때, 보통 시퀀스를 생성하여 아래와 같이 추가한다.

<insert id="insCustomer" parameterType="customerVO">
	<selectKey keyProperty="customerSeq" resultType="String" order="BEFORE">
		SELECT CUSTOMER_SEQ.NEXTVAL AS customerSeq FROM SYS.DUAL
	</selectKey>
	INSERT INTO WEB_G_CUSTOMER (
		  CUSTOMER_SEQ
		, CUSTOMER_DIV
		, EMAIL
		, PASSWORD
		, TITLE
		, CONTENT
		, REG_DT
		, PROGRESS_CD 
	) VALUES (
		  #{customerSeq}
		, '4'
		, #{email}
		, #{password}
		, #{title}
		, #{content}
		, SYSDATE
		, #{progressCd}
	)
</insert>

 

 

그렇다면,  파라메터가 리스트인 경우는 시퀀스를 어떻게 가져와서 저장해야할까?

 

 

구조상 INSERT를 할때마다 시퀀스의 NEXTVAL값을 가져와서 추가를 해야하는데,

foreach 태그 내 selectKey 태그를 사용하면 아래와 같은 에러가 뜨는 것을 확인할 수 있다.

The content of element type "foreach" must match "(include|trim|where|set|foreach|choose|if|bind)".

 

 

그렇다면 시퀀스 자동증가 테이블의 리스트 파라메터를 INSERT할 때는 다음과 같이 하면 된다 !

<update id="insFileList" parameterType="java.util.List">
	INSERT INTO WEB_G_FILE (
		  FILE_SEQ
		, FILE_PATH
		, REAL_FILE_NM
		, FILE_NM
		, FILE_EXTENSION
		, FILE_SIZE
		, REG_DT
		, USE_YN
		, BOARD_SEQ
		, BOARD_DIV
	)
	SELECT FILE_SEQ.NEXTVAL AS fileSeq
		 , T.* 
	  FROM (
	         <foreach collection="list" item="item" index="index" separator="union all">
			SELECT #{item.filePath} as filePath
			        , #{item.realFileNm} as realFileNm
			        , #{item.fileNm} as fileNm
			        , #{item.fileExtension} as fileExtension
			        , #{item.fileSize} as fileSize
			        , SYSDATE as regDt
			        , 'Y' as useYn
			        , #{item.boardSeq} as boardSeq
			        , '4' as boardDiv
			 FROM SYS.DUAL
	         </foreach> 
	  ) T
</update>

 

 

 

시퀀스 생성방법을 모른다면 다음 게시글을 참고하자 !

 

[Oracle] 자동증가 SEQUENCE 생성, 조회, 삭제

[Oracle] 자동증가 SEQUENCE 생성, 조회, 삭제 SEQ값을 키 값으로 가지고 있는 테이블을 저장할 때 MAX(SEQ)+1로 데이터를 저장하게되면 데이터가 쌓일수록 과부하가 걸린다. 따라서 시퀀스를 생성하여 시퀀스의.....

haenny.tistory.com

 

 

https://haenny.tistory.com/126  에서 퍼옴

foreach문은 사용할 때마다 사용되는 위치도 속성도 어려워서 더이상 헷갈리지 않기 위해 남겨본다 :-) !

 

MySQL foreach 사용방법은 ?

 

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL Oracle 버전이 궁금하다면 ? 클릭 ! [MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE) [MyBatis] List 파라메터 foreac.....

haenny.tistory.com

 

foreach 속성

구분 설명
collection 전달받은 인자값
item 전달받은 인자값을 다른이름으로 대체
open 해당 구문이 시작할 때
close 해당 구문이 끝날 때
index 항목의 인덱스 값을 꺼낼 때 사용할 변수 이름을 지정
separator 구분자. 한번 이상 반복할 때 반복되는 사이에 해당 문을 넣어줌

 

예제 VO

다음 예제의 파라미터로 활용되는 VO 객체이다.

public class TestVO {
    private String idx;
    private String timestampAccident;
    private String vin;
    private String dataGb;
    
    @Override
    public String toString() {
        return "TestVO [idx=" + idx + ", timestampAccident=" + timestampAccident 
 + ", vin=" + vin + ", dataGb=" + dataGb + "]";
    }
    public void setIdx(String idx) {
        this.idx = idx;
    }
    public void setTimestampAccident(String timestampAccident) {
        this.timestampAccident = timestampAccident;
    }
    public void setVin(String vin) {
        this.vin = vin;
    }
    public void setDataGb(String dataGb) {
        this.dataGb = dataGb;
    }
}

 

INSERT 구문

<insert id="insCompareResult" parameterType="java.util.List" >
    <foreach collection="list" item="item" open="INSERT ALL" close="SELECT * FROM SYS.DUAL" 
        separator=" ">
    INTO KTF_COMPARE_RESULT(
          IDX
        , TIMESTAMP_ACCIDENT
        , VIN
        , DATA_GB
    ) VALUES (
          #{item.idx}
        , #{item.timestampAccident}
        , #{item.vin}
        , #{item.dataGb}
    )
    </foreach>
</insert>
  • Line1. VO의 ArrayList로 파라메터를 넘겼기 때문에 parameterType = "java.util.List"로 설정해준다.
  • Line2-3. foreach 속성을 설정해주는 부분이다. ORACLE의 경우 멀티 INSERT 시 INSERT ALL을 해주어야 한다.

 

위의 코드는 아래와 같이 실행된다. 

INSERT ALL
    INTO KTF_COMPARE_RESULT(
          IDX
        , TIMESTAMP_ACCIDENT
        , VIN
        , DATA_GB
    ) VALUES (
          '1'
        , '1565052057'
        , 'VINVINVIN1'
        , '1'
    ) 
    INTO KTF_COMPARE_RESULT(
          IDX
        , TIMESTAMP_ACCIDENT
        , VIN
        , DATA_GB
    ) VALUES (
          '2'
        , '1565052058'
        , 'VINVINVIN2'
        , '2'
    )
SELECT * FROM SYS.DUAL

 

DELETE 구문

<delete id="delCompareResult" parameterType="java.util.List">
    DELETE FROM KTF_COMPARE_RESULT
    <where>
    <foreach collection="list" item="item" open="" close="" separator="OR">
        (IDX = #{item.idx} AND VIN = #{item.vin})
    </foreach>
    </where>
</delete>

 

위의 코드는 아래와 같이 실행된다

DELETE FROM KTF_COMPARE_RESULT
WHERE (TIMESTAMP_ACCIDENT = '1565059999' AND VIN = 'VINVINVIN1')
   OR (TIMESTAMP_ACCIDENT = '1565059999' AND VIN = 'VINVINVIN2')

 

만약 리스트 내 인자값의 따라 조건을 동적으로 주고 싶다면, 아래와 같이 foreach문 내에 if태그를 활용하면 된다.

<delete id="delCompareResult" parameterType="java.util.List">
    DELETE FROM KTF_COMPARE_RESULT
    <where>
    <foreach collection="list" item="item" open="" close="" separator="OR">
       <if test='item.dataGb==null or "".equals(item.dataGb)'>
           (TIMESTAMP_ACCIDENT = #{item.timestampAccident} AND VIN = #{item.vin})
       </if>
       <if test='item.dataGb!=null and !"".equals(item.dataGb)'>
          (TIMESTAMP_ACCIDENT = #{item.timestampAccident} AND IDX = #{item.dataGb})
       </if>
    </foreach>
    </where>
</delete>

 

MERGE 문

<insert id="insCoapLog" parameterType="java.util.List" >
    MERGE INTO KTF_COMPARE_RESULT R1
        USING (
            <foreach  collection="list" item="item" open="" close="" separator="union">
                SELECT #{item.timestampAccident} AS timestampAccident
                     , #{item.vin} AS vin
                     , #{item.objGb} AS idx
                     , #{item.dataGb} AS dataGb
                 FROM SYS.DUAL
            </foreach>
        ) T1
            ON (R1.TIMESTAMP_ACCIDENT = T1.timestampAccident)
        WHEN MATCHED THEN
            UPDATE 
            <set>
                  R1.VIN= T1.vin
                , R1.IDX= T1.idx
                , R1.DATA_GB= T1.dataGb
            </set>
        WHEN NOT MATCHED THEN
            INSERT 
            <trim prefix="(" suffix=")" suffixOverrides="," > 
                  TIMESTAMP_ACCIDENT
                , VIN
                , IDX
                , DATA_GB
            </trim>
            <trim  prefix="values (" suffix=")" suffixOverrides=",">
                  T1.timestampAccident
                , T1.vin
                , T1.idx
                , T1.dataGb
            </trim>
</insert>

 

  • Line5-10. 위의 MERGE문에서의 foreach는 list 파라미터를 가져와서 MERGE문에 사용할 테이블을 먼저 만들었다.
  • Line12. ON 조건에는 테이블의 키 값인 TIMESTAMP_ACCIDENT를 넣어주었고,
  • Line13-19. 키 값이 매칭되는 데이터가 있다면 나머지 컬럼의 데이터를 UPDATE 해준다.
    * 이 때, ON 에 넣어준 컬럼을 UPDATE에 넣어주면 에러가 난다.
  • Line20-33. 키 값이 매칭되지 않을 때는 INSERT를 해준다.

 

――――――――――――――――――――― (2020-09-15 추가) ――――――――――――――――――――――

 

UPDATE문

update 경우 파라미터 구조에 따라 여러가지 쿼리로 사용할 수 있다.

  • VO 안에 String[] 타입의 taArray 라는 배열이 있을 때 taArray 값에 따라 데이터를 업데이트 하고 싶은 경우
public class TestVO {
	private String idx;
	private String timestampAccident;
	private String vin;
	private String dataGb;
	private String[] taArr;

	... (생략)
}

 

데이터 taArr 에는 timestampAccident(키) 값을 문자열 배열로 받는 변수라고 볼 때

timestampAccident 값이 1, 2, 3,... 여러개인 경우에 idx = "idx1", vin = "vin1", dataGb="dataGb1" 과 같이 정해진 값으로 똑같이 업데이트를 하고 싶은 경우라고 생각하면 된다.

 

<update id="udtCompareResult" parameterType="testVO">
	UPDATE KTF_COMPARE_RESULT
	   SET IDX = #{idx}
	     , VIN = #{vin}
	     , DATA_GB = #{dataGb}
	 WHERE TIMESTAMP_ACCIDENT IN
	 <foreach collection="taArray" item="item" index="index" separator="," open="(" close=")">
	 	#{item}
	 </foreach>
</update>

 

위 코드는 아래와 같이 실행된다.

UPDATE KTF_COMPARE_RESULT
   SET IDX = #{idx}
     , VIN = #{vin}
     , DATA_GB = #{dataGb}
 WHERE TIMESTAMP_ACCIDENT IN ("1", "2", "3", ...) 

 

WHERE IN 문 뒤에 "open" 값인 "(" 로 시작하여 #{item} 값과 "separator"로 설정한 "," 구분자로 값을 그대로 작성한 뒤 "close"로 ")" 닫아준다.

 

 

일반적인 리스트 VO를 넘겼을 때의 경우이다.

<update id="udtCompareList" parameterType="java.util.List">
    <foreach collection="list" item="item" index="index" separator=";" open="DECLARE BEGIN" close="; END;">
        UPDATE KTF_COMPARE_RESULT 
           SET IDX = #{item.idx}
	     , VIN = #{item.vin}
	     , DATA_GB = #{item.dataGb}
         WHERE TIMESTAMP_ACCIDENT = #{item.timestampAccident}
    </foreach>
</update>

 

 

위 코드는 아래와 같이 실행된다.

DECLARE BEGIN
	UPDATE KTF_COMPARE_RESULT
	   SET IDX = #{item.idx}
	     , VIN = #{item.vin}
	     , DATA_GB = #{item.dataGb}
	 WHERE TIMESTAMP_ACCIDENT = #{item.timestampAccident}
	;
	UPDATE KTF_COMPARE_RESULT
	   SET IDX = #{item.idx}
	     , VIN = #{item.vin}
	     , DATA_GB = #{item.dataGb}
	 WHERE TIMESTAMP_ACCIDENT = #{item.timestampAccident}
	;
	.
	.
	.
	UPDATE KTF_COMPARE_RESULT
	   SET IDX = #{item.idx}
	     , VIN = #{item.vin}
	     , DATA_GB = #{item.dataGb}
	 WHERE TIMESTAMP_ACCIDENT = #{item.timestampAccident}
; END;

 

 

자주나는 에러

Caused by: org.apache.ibatis.binding.BindingException: Parameter '__frch_item_0' not found. Available parameters are [collection, list]

 

참고로 이 에러는 보통 VO 컬럼명(파라메터 컬럼명)의 오타일 가능성이 가장 크다

 

 

https://haenny.tistory.com/21  에서 퍼옴 정리 잘되어 있음

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL

 

 

Oracle 버전이 궁금하다면 ? 클릭 !

 

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE)

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE, MERGE) foreach문은 사용할 때마다 사용되는 위치도 속성도 어려워서 더이상 헷갈리지 않기 위해 남겨본다 :-) ! foreach문의 속성은 다음과 같다......

haenny.tistory.com

 

 

 

foreach문의 속성은 다음과 같다.

 

구분 설명
collection 전달받은 인자값
item 전달받은 인자값을 다른이름으로 대체
open 해당 구문이 시작할 때
close 해당 구문이 끝날 때
index 항목의 인덱스 값을 꺼낼 때 사용할 변수 이름을 지정
separator 구분자. 한번 이상 반복할 때 반복되는 사이에 해당 문을 넣어줌

 

 

 

 

다음 예제의 파라미터로 활용되는 VO 객체이다.

 

 

public class TestVO {
    private String idx;
    private String timestampAccident;
    private String vin;
    private String dataGb;
    
    @Override
    public String toString() {
        return "TestVO [idx=" + idx + ", timestampAccident=" + timestampAccident 
 + ", vin=" + vin + ", dataGb=" + dataGb + "]";
    }
    public void setIdx(String idx) {
        this.idx = idx;
    }
    public void setTimestampAccident(String timestampAccident) {
        this.timestampAccident = timestampAccident;
    }
    public void setVin(String vin) {
        this.vin = vin;
    }
    public void setDataGb(String dataGb) {
        this.dataGb = dataGb;
    }
}

 

 

 

 

INSERT 문

 

 

<insert id="insCompareResult" parameterType="java.util.List" >
    INSERT INTO KTF_COMPARE_RESULT(
          IDX
        , TIMESTAMP_ACCIDENT
        , VIN
        , DATA_GB
    ) VALUES 
    <foreach collection="list" item="item" separator=",">
    (
          #{item.idx}
        , #{item.timestampAccident}
        , #{item.vin}
        , #{item.dataGb}
    )
    </foreach>
</insert>

 

 

 

 

DELETE 문

 

 

- 파라메터가 리스트인 경우

 

<delete id="delCompareResult" parameterType="java.util.List"> 
	DELETE FROM KTF_COMPARE_RESULT 
	<where> 
		<foreach collection="list" item="item" open="" close="" separator="OR"> 
		(IDX = #{item.idx} AND VIN = #{item.vin}) 
		</foreach> 
	</where> 
</delete>

 

 

- 파라메터가 배열인 경우

 

<delete id="delCompareResult" parameterType="java.util.Arrays"> 
	DELETE FROM KTF_COMPARE_RESULT 
	<where> 
		TIMESTAMP_ACCIDENT IN
		<foreach collection="array" item="item" index="index" separator="," open="(" close=")"> 
			#{item}
		</foreach> 
	</where> 
</delete>

 

 

https://haenny.tistory.com/182 에서 퍼옴

의존성(dependency) 추가

implementation "org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16"

 

프로퍼티 파일 추가

파일 : log4jdbc.log4j2.properties

경로 : src/main/resources

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

 

datasource 설정

  • 설정 파일 : application.yml
  • driver-class-name 추가
  • jdbc url 변경 : "log4jdbc:" 추가
spring:
	datasource:
		driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
		url: jdbc:log4jdbc:postgresql://localhost:5432/test

 

설정해준 뒤 실행시키면 파라미터가 세팅되지 않은 쿼리와 파라미터 값을 세팅한 쿼리로그가 찍힐 것이다.

 


관련 참고 게시글 : JPA 쿼리 로그 설정

 

[JPA] SpringBoot JPA 쿼리 로그 설정하기

SpringBoot JPA를 사용할 때, SQL 쿼리문을 표출하는 설정 관련해서 정리해보자. 요약 application.yml spring: jpa: properties: hibernate: show_sql: true format_sql: true use_sql_comments: true logging: l.....

haenny.tistory.com

 

 

https://haenny.tistory.com/398 에서 퍼옴 글 정리 잘되어 있음

 <sql>

다른 구문에서 재사용하기 위한 SQL 조각

 사용 조건

  • id 속성 값 필수
  • 사용하려는 태그의 위치보다 먼저 선언되어야 함

 문법

<sql id="userColumns"> 
	${alias}.id,
	${alias}.username,
	${alias}.password 
</sql>

 

 <include>

SQL 조각인 <sql> 문을 DML (Data Manipulation Language) 태그, 즉, 다른 구문에 포함(삽입) 시키는 기술

 문법

<select,insert,update,delete>
	<include refid="<sql> id">
		<property name="<sql> property" value=""/>
	</include>
</select,insert,update,delete>

 

 예제

 Where 문법 재사용

<sql id="where">
	where board_id = #{boardId}
</sql>

<select id="getPage" resultType="int">
	select pageNum 
	from m_board 
	<include refid="where"></include>	
</select>

 

 Table 문법 재사용

<sql id="returnTable">
	from ${tableproperty}
</sql>

<select id="getPage" resultType="int">
	select pageNum 
	<include refid="returnTable">
		<property name="tableproperty" value="m_board"/>
	</include>
</select>

 

 JOIN문 활용

<sql id="userColumns"> 
	${alias}.id,
	${alias}.username,
	${alias}.password 
</sql>

<select id="selectUsers" resultType="map">
	select
	   <include refid="userColumns"><property name="alias" value="t1"/></include>,
	   <include refid="userColumns"><property name="alias" value="t2"/></include>
	from some_table t1
	cross join some_table t2
</select>

 

 SELECT 컬럼 문법 재사용

<sql id="common_select_table">
	id, name, age
</sql>

<select id="getMemberInfo">
	SELECT
	   <include refid="common_select_table" />
	FROM TABLE
</select>

 

 INSERT 문 활용

<sql id="board_columns">
    ${alias}id,
    ${alias}subject,
    ${alias}context,
    ${alias}attachments,
    ${alias}likes,
    ${alias}views,
    ${alias}create_time,
    ${alias}update_time,
    ${alias}writer
</sql>

<insert id="insertBoard">
    INSERT INTO BOARD (
           <include refid="board_columns"><property name="alias" value=""/></include>
         ) VALUES (
          #{id},
          #{subject},
          #{context},
          #{attachments},
          #{likes},
          #{views},
          now(),
          null,
          #{writer} )
</insert>

 

 외부 SQL-Mapper.xml 접근 + <include>

  • common-Mapper.xml
<mapper namespace="mapper.common-Mapper">

	<sql id="board_col">
		${alias}id,
		${alias}subject,
		${alias}context
	</sql>

</mapper>
  • board-Mapper.xml
<select id="selectUser">
    SELECT 
	<include refid="mapper.common-Mapper.board_col">
		<property name="alias" value="b."/>
	</include>
	FROM board b
</select>

사용할 외부 Mapper.xml 에 namespace.<sql>id 로 가져오면 된다.

 

sql과 include 태그를 사용해본적이 없는데, 앞으로 사용하게 될 문법이라고 생각해서 살펴봤다.

코드의 재사용성을 높이기에 좋은 문법이라고는 하지만, 단순히 살펴봤을 때 가독성이 좋아보이진 않는다..

제대로 사용해봐야 알것같다..

 


MyBatis

 

MyBatis – 마이바티스 3 | 매퍼 XML 파일

Mapper XML 파일 마이바티스의 가장 큰 장점은 매핑구문이다. 이건 간혹 마법을 부리는 것처럼 보일 수 있다. SQL Map XML 파일은 상대적으로 간단하다. 더군다나 동일한 기능의 JDBC 코드와 비교하면...

mybatis.org

https://java119.tistory.com/102

 

[MyBatis] 개발 생산성 향상,중복 쿼리 줄이기 <sql>,<include> 개념 및 문법 총 정리

개념 다른 구문에서 재사용하기 위한 SQL 조각 출처 : mybatis 공식 사이트 말 그대롭니다. "재사용성을 높이기 위한 SQL 조각" 아주 정확한 표현입니다. 조건 1.id 속성 값이 필수입니다....

java119.tistory.com

 

 

https://haenny.tistory.com/372 에서 펌 정리 잘되어 있는 사이트 ㅎ

'study > java' 카테고리의 다른 글

[MyBatis] List 파라메터 foreach 사용 (INSERT, DELETE) - MySQL  (0) 2023.07.12
[MyBatis] SpringBoot MyBatis 쿼리 로그 설정하기  (0) 2023.07.12
javascript 페이징  (0) 2023.07.05
sync post 데이터 획득  (0) 2023.07.05
jsp 지시태그  (0) 2023.07.05
<script>  
//페이지에 대한 리스트 호출 
fn_pagingEvt = function(discrhash){
	/* $(".numbtns").removeClass("on");
	$("#paging1_"+page).addClass("on"); 
	var param1 = $("#param1 option:selected").val(); 
	var param2 = $("#param2").prop('value');
	var param3 = $("#param3 option:selected").val(); */
	
	$.ajax({url : "/config/mentoData2",type : "GET",datatype : "json",contentType: "text/html; charset:UTF-8",
		data:{"DI":discrhash},
	success : function(data) { 
						var res = JSON.parse(data);
						var htmls = "";
						 
						for(var i=0;i<re.length;i++){
							/*
							 tbl = re[i];
							 console.log(tbl)
							 var pnum = parseInt(listCnt)-(parseInt(start_num)+parseInt(i));							 
							 htmls +="<tr>";
							 htmls +="    <td class='table_in col_1'>"+pnum+"</td>";
							 htmls +="    <td class='table_in col_2'>"+tbl.PART+	"</td>";
							 htmls +="    <td class='table_in col_3'>"+tbl.ITEMS_ITEM0+"</td>";
							 htmls +="    <td class='table_in col_4'>"+tbl.WRT_TM.substr(0,10).replace(/-/gi,".")+"</td>"; 
							 htmls +="</tr>";  
							 */
						}
						$("#bodyContent").html(htmls); 
		},
		error : function() {alert('error');}
	}); 
}  
//fn_pagingEvt("${discrhash}");
</script>
async function post(host, path, body, headers = {}) {
  const url = `https://${host}/${path}`;
  const options = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...headers,
    },
    body: JSON.stringify(body),
  };
  const res = await fetch(url, options);
  const data = await res.json();
  if (res.ok) {
    return data;
  } else {
    throw Error(data);
  }
}

post("jsonplaceholder.typicode.com", "posts", {
  title: "Test",
  body: "I am testing!",
  userId: 1,
})
  .then((data) => console.log(data))
  .catch((error) => console.log(error));
<%-- 지시 태그  --%>
<%@page import="java.util.ArrayList"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>태그 예제</title>
</head>
<body>
        
    <%@ include file ="header.jsp" %>
    
        <%--선언 태그 (멤버변수, 메서드) --%>
    <%! 
    int num = 10;
    String str = "Hello";
    ArrayList<String> list = new ArrayList<>();
    %>
    <%!
    public static void tagMethod() {
    %>
      <p> hello Tag </p>
    <%! 
    }
    %>
    
    <!-- HTML 주석 태그 -->
    <%-- JSP 주석 태그 --%>
    
    <%-- 스크립트릿 태그 --%>
    <%
    if(num > 0) {
    %>
    <p> num > 0 </p>
    <%
    }else {
    %>
    <p> num <=0 </p>
    <%
    }
    %>
    
    <%-- 표현식 태그 --%>
    num is <%= num %> <br><br>
    str is <%= str %>
    
    
    <%@ include file ="footer.jsp" %>
</body>
</html>
 

기본Spec

EgovFramework 4.0 - 64bit

Java 1.8

Tomcat 8.0

Gradle Project SpringBoot 2.6.5

 

전자정부 프레임워크 4.0 설치

https://haenny.tistory.com/294

 

[EgovFramework] 전자정부프레임워크 4.x 개발환경 다운로드 및 Lombok 설치

[EgovFramework] 전자정부프레임워크 4.x 개발환경 다운로드 및 Lombok 설치 전자정부프레임워크 다운로드 전자정부프레임워크 4.x 개발환경 다운로드 받으러가기 개발환경 - 4.x 다운로드 | 표준프레임...

haenny.tistory.com

 

JDK (Java) 설치

https://haenny.tistory.com/219

 

[Java] Windows10 환경 OpenJDK 설치 및 환경설정

[Java] Windows10 환경 OpenJDK 설치 및 환경설정 1. OpenJDK 다운로드 OpenJDK는 다운로드 는 아래 링크를 통해 원하는 버전의 설치 파일을 다운로드 할 수 있다. JDK Builds from Oracle jdk.java.net JDK 배포.....

haenny.tistory.com

 

Tomcat 8.0 설치 및 서버 설정

다음 게시글에서 Tomcat 관련 글을 참고하면 된다.

https://haenny.tistory.com/50

 

[Eclipse] 전자정부프레임워크 3.8 버전 업그레이드 하기 (1) Spring

[Eclipse] 전자정부프레임워크 3.8 버전 업그레이드 하기 (1) Spring 기존 egovFramework 3.6.0 버전에서 개발을 하고 있었는데 3.8버전을 업그레이드 하려한다. 분명 언젠가 또 업그레이드 할 날이 올 것을...

haenny.tistory.com

 

SpringBoot Gradle Project 간편하게 생성

https://start.spring.io

 

 

본인이 만드려는 프로젝트명과 그룹명을 설정하고 스펙에 맞게 입력 후 Generate CTRL + Enter  버튼을 클릭하면 압축(Zip) 파일이 다운로드 될 것이다.

 

 

 

다운받은 압축파일은 본인의 이클립스 workspace 경로에 옮긴 뒤에 "여기에 압축풀기" 를 클릭하여 압축을 풀어야 프로젝트 임포트할 때 편리하다.

 

 

 

다시 이클립스로 돌아와서 Package Explorer 창에서 마우스 우키 - Import - Gradle - Existing Gradle Project  클릭하고,

다음 화면에서 Browser 를 선택하여 이전에 압축해제한 파일을 선택한 뒤 Finish를 클릭한다.

 

 

 

 

 

그러면 다음과 같은 구조로 프로젝트가 생성된다 !

 
 

 

 


Dependency 추가하기

주로 SpringMVC 구조의 프로젝트를 진행하면서 자주 사용해왔던 라이브러리를 위주로 추가할 것이다.

 

버전에 따라 필요한 라이브러리는 Maven Repository 에서 검색한 뒤 알맞은 버전을 선택해서 build.gradle dependencies 에 추가해주면 된다.

 

만약 jstl 를 추가할 때 여러개가 나와서 헷갈릴 수 있다.

첫 번째 것으로 추가를 하고 Gradle - Refresh Gradle Project 를 클릭하면, build.gradle 파일에 빨간 X 표시가 되어 정상적으로 추가되지 않을 것이다.

 

javax.servlet.jsp.jstl 로 추가했을 때 build.gradle 에러

 

 

두 번째 JSTL인 (javax.servlet.jstl) 로 받아야한다.

 

 

필자는 가장 최신 버전인 1.2 버전을 클릭하였고, Gradle Tab 의 implementation group 정보를 그대로 복사하여 build.gradle 에 추가해주면 된다.만약 지금 게시물과는 번외로, Maven Project 인 경우에는 Maven Tab을 클릭하여 추가해주면 된다.

 

 

 

 

의존성 추가 화면 

 

 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
	// web
	implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.6.5'
	// jstl    
	// https://mvnrepository.com/artifact/javax.servlet/jstl    
	implementation group: 'javax.servlet', name: 'jstl', version: '1.2'

	// 내장 tomcat을 사용하기 때문에 JSP 처리하는 서블릿을 추가하는 것
	// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-jasper
	implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '9.0.60'    
	
	// lombok
	// https://mvnrepository.com/artifact/org.projectlombok/lombok
	compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
	annotationProcessor 'org.projectlombok:lombok:1.18.12'
    
	// 정적 소스 재시작 없이 적용
	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools
	implementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.6.5'
	
	// simple json
	// https://mvnrepository.com/artifact/com.google.code.gson/gson
	implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0'
	// https://mvnrepository.com/artifact/com.googlecode.json-simple/json-simple
	implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'
}

 

application.properties 설정

아직은 데이터베이스 커넥션 없이 프로젝트를 사용을 할 것이기 때문에, 웹프로젝트 특성에 맞는 일부 설정만 하였다.

 

 

 
#project name
project.name=TestBoot

#WEB 환경설정
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
server.port=9090
server.servlet.context-path=/TestBoot

#정적소스 재시작 없이 적용
spring.devtools.livereload.enabled=true
spring.freemarker.cache=false

 

 

여기까지 웹 프로젝트를 구현하기 위한 간단한 설정은 끝났다.

이제 실제 페이지 화면 (jsp) 와 컨트롤러를 연결하기 위해 파일을 추가하보자.

 

 


 

webapp 구조 폴더 생성

 

 

 

main.jsp

 

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!doctype html>
<html lang="ko">
<head>
	Hellow World !
</head>
<body>
<script>
</script>
</body>
</html>

 

 

PageController

 

 

package kr.co.haenny.TestBoot.controller;

import java.util.Enumeration;

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

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageControl {	
	@GetMapping("/**/*.do")
	public String goToGetPage(HttpServletRequest request, HttpServletResponse response, Model model){
		if(request.getServletPath().equals("/error/page.do")) {
			/*에러 페이지인 경우 상태코드와 상태메세지 리턴*/
			HttpStatus hs = HttpStatus.valueOf(response.getStatus());
			model.addAttribute("status", hs.value());
			model.addAttribute("message", hs.getReasonPhrase());
		}else {		
			/*파라미터있는 경우 데이터 리턴*/
			Enumeration<String> params = request.getParameterNames();
			while (params.hasMoreElements()) {
				String name = (String) params.nextElement();
				model.addAttribute(name, request.getParameter(name));
			}
		}
		return request.getServletPath().replace(".do", "");
	}
}

 

 

사실 Page Controller는 전자정부프레임워크 프로젝트에서 사용해왔던 PageController를 그대로 가져왔는데, 

이렇게 적용한 뒤 실행을 시키려니 아래와 같은 에러가 났으니 참고바란다.

 

 

 

 

그 외 SpringBoot 오류  관련된 내용 참고하자.

 

https://haenny.tistory.com/296 Initializing Spring DispatcherServlet 'dispatcherServlet'

 

SpringBoot Initializing Spring DispatcherServlet 'dispatcherServlet'

SpringBoot 실행 후 웹페이지 로드 시에 표출되는 로그 Initializing Spring DispatcherServlet 'dispatcherServlet' Initializing Servlet 'dispatcherServlet' Completed initialization in 1 ms application.pr.....

haenny.tistory.com

https://haenny.tistory.com/298 Failed to determine a suitable driver class

 

Failed to determine a suitable driver class 오류 해결

오류 로그 ********************************* APPLICATION FAILED TO START ********************************* Description: Failed to configure a DataSource: 'url' attribute is not specified and no embed.....

haenny.tistory.com

https://haenny.tistory.com/299 "Path with "WEB-INF" or "META_INF" : [WEB-INF/jsp/main.jsp]" 

 

SpringBoot "Path with "WEB-INF" or "META_INF" : [WEB-INF/jsp/main.jsp]" 경고 및 Whitelabel Error Page 오류

"Path with "WEB-INF" or "META-INF" : [WEB-INF/jsp/main.jsp]" SpringBoot 에서 main.jsp 화면을 만들고, Controller에서 View로 리턴을 하였는데 jsp 파일을 찾지 못하는 경우 ResourceHttpRequestHandler : P.....

haenny.tistory.com

 

 

 
 
좋아요1
게시글 관리
 구독하기

 

SpringBoot "Path with "WEB-INF" or "META_INF" : [WEB-INF/jsp/main.jsp]" 경고 및 Whitelabel Error Page 오류  Failed to determine a suitable driver class 오류 해결  SpringBoot 2.5 → 2.6 업그레이드 시No more pattern data allowed after {*...} or ** pattern element 오류 해결  SpringBoot Initializing Spring DispatcherServlet 'dispatcherServlet'  [SpringBoot] 스프링부트 properties 설정파일 분리하기  

 

[AWS, 웹 프로젝트] 3. STS에서 Gradle Project 생성하기

 엠케이  2019. 5. 6. 17:43

이 번 프로젝트 시작하면서 Spring Tool Suite(STS) 를 이용하게 되었다. 프로젝트를 생성하고 빌드시켜 실행해 보았는데, 어렵지 않고 편리했다.

처음 프로젝트를 생성할 때 빌드도구와 java 버전 선택을 한다. 그리고 개발에 필요한 의존성 설정을 플젝 시작부터 때려넣을 수 있었다. 물론 의존성 설정 없이 빈 값으로 프로젝트를 생성해도 상관없다.

1. Gradle을 이용한 Project를 생성해보자.

1. [File] - [New] - [Spring Starter Project]

Spring Tool Suite 4 를 깔으면 Gradle 3.x 버전이 이미 깔려있다. 2.x 는 안깔려있음.

나는 Buildship 3.x로 선택 ! Java Version 은 8, Language는 Java로 선택한다. 코틀린과 그루비도 선택이 가능하다. 둘다 할 줄 모름

 

Finish 로 프로젝트 생성을 해본다.

[Build.gradle] 을 열어보면 아래와같이 설정되어있다.

plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' id 'war' } apply plugin: 'io.spring.dependency-management' group = 'com.walwal' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'mysql:mysql-connector-java' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' }

그냥 이대로 실행을 해 본다.

실패가 난다.

Failed to configure a DataSource (실패. 설정. 데이터소스)


***************************

APPLICATION FAILED TO START

***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

Action:

Consider the following:

If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.

If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


오류 발생 이유 : 처음에 Gradle 프로젝트 생성시 DB 관련된 dependencies를 추가했는데 설정이 되어있지 않아서 그렇다.

해결 방법 : 

Database 설정 추가 해야 한다.

src/main/resources/application.properties에 database 정보를 추가해준다.

추가하려는데 로컬에 DB (MySQL)를 안깔아둠. MySQL 을 빠르게 설치해본다.

로컬에 MySQL 설치하기 !

https://blog.naver.com/mering_k/221530886538

DB가 로컬에 설치가 되었다면 연결을 할 수 있다.

src/main/resources에 있는 [application.properties] 파일에 아래와같이 mysql 설정사항을 적어준다.

[application.properties]

spring.datasource.url=jdbc:mysql://localhost:3306/world spring.datasource.username=root spring.datasource.password=lalalapasswdzzzz spring.datasource.driver-class-name=com.mysql.jdbc.Driver

또 오류가 난다. 아래는 오류 로그이다.


Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.


해결 방법 : com.mysql.jdbc.Driver-class-name 을 com.mysql.cj.jdbc.Driver 로 바꿔준다.

[application.properies]

spring.datasource.url=jdbc:mysql://localhost:3306/world spring.datasource.username=root spring.datasource.password=lalalapasswdzzzz spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

변경 전: com.mysql.jdbc.Driver

변경 후: com.mysql.cj.jdbc.Driver

이제 진짜 레알 다시 실행 !!!!!!!!

오류가 없이 제대로 실행완료 되었다.

2. JSP 와 연동하여 브라우저로 접속해보자 !

간단하게 서버에 올린 프로젝트가 잘 실행 되는지 테스트 하기 위해서 [testController.java] 파일과 [test.jsp]파일을 생성하고 간단하게 코드를 작성해본다. 브라우저에 뭔가를 띄워 봐야 제대로 성공한 느낌이 나니까..

test.jsp 위치는 아래와같이 webapp/WEB-INF/views 폴더 생성 후 JSP 를 만들어준다.

아, 물론 프로젝트는 JSP 를 이용하지 않고, 단일페이지 어플리케이션(SPA)으로 개발 할 예정이다.

사실 아직 React 랑 스프링연동하는 법 공부 안함. 아 물론 React도 공부 아직 안함.

[testController.java]

package com.walwal.mall.test.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class testController { @RequestMapping("/test") public String test(){ return "test"; } }

[test.jsp]

<html> <h4> localhost:8080 </h4> <h1> 성공 ! 야호 ! </h1> </html>

Controller 과 JSP 를 연결하기 위해 application.properties 에 설정을 추가해 준다.

prefix, suffix 설정 추가해준다.

[application.properties]

spring.dat... ( 데이터연결설정 ) spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp

근데, 한글이 깨져버림.

[application.properties]에 설정 추가 ( 기존에 적혀있는거 아래에 추가로 적어주면 된다. )

spring.dat... ( 데이터연결설정 ) spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp spring.http.encoding.charset=UTF-8

[test.jsp] 에 아래와같이 pageEncoding 추가

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <h4> localhost:8080 </h4> <h1> 성공 ! 야호 ! </h1> </html>

+ 만약 설정을 다 잘 한거 같은데, 자꾸 페이지에 404 에러가 뜬다면 애노테이션을 잘 붙혔는지 확인해본다.

@Controller 붙히는건 잊지 않았는지.. 과연 잊을사람이 있을까 싶지만 그게바로 나다.

STS를 이용하여 Gradle 프로젝트 만들기 완성! 짝짝짝짝

다음에는 DB와 연결해서 데이터뿌리기 도전 !!

스프링부트+gradle+JSP+STS(Eclipse)

sts(spring tool suit) 에서 스프링부트+gradle+jsp 세팅

by 크리스턍 2022. 4. 25.

1. gradle 설치

 gradle 을 다운 받아서 압축을 해제한다.

 

 1) gradle 다운로드 사이트 https://gradle.org/releases/

 2) gradle 사이트에 접속하여 binary-only 로 파일을 다운로드한다.

 3) gradle-7.4.2-bin.zip 파일이 다운로는 되는데 해당 압축 파일을 원하는 폴더에 압축을 풀어준다.

 

2. STS gradle 연동

 STS 를 실행 후 환경 설정에서 gradle 을 수동으로 설정해 준다.

 1) STS 실행후 Window -> Preferences 창을 열어준다.

 

 2) 좌측 메뉴에서 Gradle 을 선택, Local installation directory 에 gradle 을 수동 설치한 경로를 지정해 준다.

 

3. 프로젝트 생성

 스프링 프로젝트를 생성한다.

 

 1) Srping Starter Project 선택하여 스프링 프로젝트 생성을 진행한다.

 2) Type : Gradle Project

    Package : Jar 를 선택시 Java 관련 리소스만 패키징 되기때문에 War 를 선택

 3) Dependencies 선택을 할 경우 프로젝트 생성시 최초 build.gradle 관련 라이브러리들이 자동으로 설정 된 상태로 프로젝트가 생성된다. 최초 Lombok 과 Spring Web 만 선택을 하고 필요한 라이브러리들은 추후 build.gradle 파일에서 추가하면 된다.

 

4. 인코딩 설정

 프로젝트의 encoding 타입을 을 설정한다

 1) 인코딩 타입을 UTF-8 로 설정하지 않으면 한글 입력이 되지 않을 수 있다. (EUC-KR 을 써도 됨)

 

5. 웹 화면 관련 설정

 1) 프론트 단을 구성을 JSP 로 하기때문에 JSP 를 사용할 수 있는 라이브러리를 추가해 준다.

	// JSP
	implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
	
	// JSTL
	implementation 'javax.servlet:jstl'

 2) 컨트롤러에서 호출할 뷰 페이지 경로와 접미사(파일확장자명) 을 application.properties 파일에 설정해 준다.

# 컨트롤러에서 호출할 뷰 페이지 경로
spring.mvc.view.prefix=/WEB-INF/views/
# 컨트롤러에서 호출할 뷰 페이지 접미사(파일확장자명)
spring.mvc.view.suffix=.jsp

 

6. STS 에 웹 개발 툴 설치

 

 1) Help -> Eclipse Marketplace 에 들어가 Eclipse Enterprise Java And Web Developer Tools 를 install 해준다.

    위에 첨부한 이미지와 동일하게 진행 해주면 된다. 설치가 완료 후에는 sts 를 재시작 해 준다.

 

7. 테스트 코드 작성

 1) 테스트할 화면 페이지 소스를 작성한다. 이미지와 동일한 경로에 폴더와 파일을 생성해 준다.

<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
인덱스 페이지!!!!
</body>
</html>

 

 2) 테스트 할 컨트롤러 소스를 작성한다. 위미지와 동일한 경로에 패키지와 파일을 생성해 준다.

package com.example.demo.sample.controller;

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

@Controller
public class SampleController {
	@RequestMapping(value="/", method = RequestMethod.GET)
	public String index() {
		System.out.println("인덱스 페이지 호출");
		return "index";
	}

}

8. 서버 실행 및 테스트

 1) Boot DashBoard 에서 프로젝트 명(gradleProject) 를 선택후 Start 또는 Debug 로 서버를 실행한다.

    Start : 일반적인 서버 실행

    Debug : 디버깅이 필요할 경우 디버그 모드로 서버 실행

 

 2) 브라우저 창을 연 후 http://localhost:8080 으로 접속하면 작성한 소스의 페이지가 호출되는 것을 확인할 수 있다.

좋아요2
공유하기
게시글 관리
구독하기

[스프링부트 (4)] Spring Boot DataBase 연동하기 (MariaDB, MyBatis, HikariCP)

by 갓대희 2020. 2. 12.
 

[스프링부트 (4)] 스프링부트 DB 연동 (MariaDB, MyBatis, HikariCP)

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ SpringBoot  DB 연동입니다. : ) 

 

 

0. 들어가기 앞서

Spring Boot를 사용하면서 DB를연결하기 위해 JDBC Connection Pool이란걸 사용 해보셨을 것이다.

 

▶ 커넥션풀(Connection Pool)이란?

1) 정의

 - 풀(Pool)속에 데이터베이스와의 연결(커넥션)들을 미리 만들어 두고 데이터베이스에 접근시 풀에 남아있는 커넥션중 하나를 받아와서 사용한뒤 반환하는 기법.
 - DataBase Connection Pool, DBCP라고도 한다.

 

2) 사용이유

 - 웹 애플리케이션은 다수의 사용자가 데이터베이스에 접근해야 하는 상황에 사용자들이 요청할때마다 연결을 만들고 해제하는 과정을 진행하게되면 비효율적이다.
 따라서 커넥션풀을 이용하여 미리 여러 연결을 만들어놓고 필요한 사용자가 요청시 미리 만들어놓은 연결을 주는 형식으로 효과적으로 DB연결 및 자원사용을 할 수 있다.

 

Spring Boot에 JDBC를 통해 mariadb(mysql) 연결해보자!


사실 특별한 설정이 필요하진 않다. 역시 스프링 부트 하다.
Dependency와 application.properties에 간단한 설정만 하면 MyBatis 및 MariaDB 연결은 완료 된다. (1 ~ 2번)
앞서 포스팅한 MVC의 기본 개념들을 적용하여 화면에 출력하는 것 까지 간단한 예제로 정리해 보려 한다.

 

 

1. build.gradle

▶ Dependency 추가

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'

 

- maven의 경우(보관용)

<!-- MariaDB -->
<dependency>
	<groupId>org.mariadb.jdbc</groupId>
	<artifactId>mariadb-java-client</artifactId>
    <version>3.0.6</version>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

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

 

2. application.properties

▶ driver 및 연결 정보 설정 추가

spring.datasource.driverClassName=org.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=스키마계정
spring.datasource.password=비밀번호

#mariadb
#spring.datasource.driverClassName=org.mariadb.jdbc.Driver
#spring.datasource.url=jdbc:mariadb://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC

#oracle
#spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:1532/test

 

3. testMapper.xml 생성

▶ mapper 추가

 - 경로 : resources\mybatis\test\testMapper.xml
 - 주의 : 향후 생성할 mapper interface에 대한 풀패키지 경로가 필요하고, 각 쿼리문의 id값과 mapper interface의 메서드명과 일치 해야 한다. 

ex) 

<?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">
<mapper namespace="com.god.bo.test.mapper.TestMapper">
    <select id="selectTest"  resultType="com.god.bo.test.vo.TestVo">
        SELECT 'GOD' AS NAME
    </select>
</mapper>

 

※ 여기서 간단한 설정을 통해 resultType에 계속 풀패키지를 명시하지 않도록 할 수 있다.
1) application.properties 추가

# mybatis 매핑 type을 짧게 쓰기 위한 설정 
# mapper.xml에서 resultType을 지정할 때 com.god.bo.test.vo.TestVo 대신 TestVo로 간략히 할 수 있다. 
mybatis.type-aliases-package=com.god.bo.test.vo 
# mapper.xml 위치 지정 
# **은 하위 폴더 레벨에 상관없이 모든 경로를 뜻하며, *는 아무 이름이나 와도 된다는것을 뜻합니다. 
mybatis.mapper-locations=mybatis/**/*.xml

 

2) mapper.xml 에서 resultType에 클래스명만 명시

<select id="selectTest"  resultType="TestVo">
	SELECT 'GOD' AS NAME
</select>

 

 

4. Vo 클래스 생성(model, dto, vo)

 - 경로 : com.god.bo.test.vo.TestVo
 - 데이터를 관리하는 클래스, 데이터를 View로 넘겨줄때 사용하는 객체. 흔히 관념적으로 모델이라고 하며 DTO, VO 라는 표현을 많이 쓴다.

ex)

package com.god.bo.test.vo;

public class TestVo {
    private String id;
    private String name;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

 

 

5. Mapper 인터페이스 생성

 - 경로 : com.god.bo.test.mapper.TestMapper
 - 주의 : mapper.xml의 mapper tag 안에 선언한 namespace 에 정확하게 일치하는 위치에 같은 이름으로 생성해야 한다.
 그리고 select tag 안에 선언한 id 값과 각 method 이름을 같게 생성해야 한다.

ex)

package com.god.bo.test.mapper;

import java.util.List;

import com.god.bo.test.vo.TestVo;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface TestMapper {
    List<TestVo> selectTest();
}

 

 

6. Service 클래스 생성

 - TestMapper interface를 autowired 하여 메서드를 콜한다.

ex) 

package com.god.bo.test.service;

import com.god.bo.test.mapper.TestMapper;
import com.god.bo.test.vo.TestVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class TestService {

    @Autowired
    public TestMapper mapper;

    public List<TestVo> selectTest() {
        return mapper.selectTest();
    }
}

 

 

7. Controller 생성 및 Service 호출

 - 위에서 생성한 Service의 메서드를 호출하는 Controller를 생성 하도록 하자.

ex) 

@Autowired
TestService testService;

@RequestMapping(value = "/test")
public ModelAndView test() throws Exception{
    ModelAndView mav = new ModelAndView("test");

    List<TestVo> testList = testService.selectTest();
    mav.addObject("list", testList);

    return mav;
}

 

 

8. 화면에 맞게 출력하자.

ex)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>View Test Page</title>
</head>
<body>
    <h2>Hello!</h2>
    <div>JSP List Test</div>
    <c:forEach var="item" items="${list}" varStatus="idx">
        ${idx.index}st, Hello! ${item.name} <br />
    </c:forEach>
</body>
</html>

 

Application을 실행하여 해당하는 경로의 페이지를 호출하여 보자.

올바르게 데이터 베이스가 연동 되어, 화면에 노출되는것을 볼 수 있다. 

여기까지 했다면 Mybatis, MariaDB연동에 성공 하였다.

 

 

9. HikariCP

1) Spring Boot 2.0 이전 : Tomcat JDBC Connection Pool를 Default로 사용하였다.
2) Spring Boot 2.0 이후 : HikariCP를 Default로 사용하고 있다.
3) hikariCP github사이트에서는 매우 빠르고, 가볍고, 신뢰할 수 있다고 설명한다.
4) "zero-overhead" 엄청나게 높은 성능이라고 강조하며 tomcat, dbcp 등과의 성능 비교 결과도 첨부하고 있다.

사진 출처 : https://github.com/brettwooldridge/HikariCP



결국 예전 버전에서는 명시하여 사용해야했지만, 이제는 application.properties에 간단한 설정만 한다면 쉽게 사용할 수 있다.

 

▶ 설정 예시

#MariaDB
spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=UTC
#Hikari
spring.datasource.hikari.username=username
spring.datasource.hikari.password=password
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.connection-test-query=SELECT 1

 

혹시나 하여 HikariConfig클래스 파일도 확인해 보았다.

여러 프로퍼티를 설정할수 있는 것 같다.

 

▶ HikariCP 옵션

 -  HikariCP 옵션 (다음 github에서 더 자세히 나와 있다.)

참고 : https://github.com/brettwooldridge/HikariCP 

 - HikariCP설정의 시간 단위는 밀리세컨즈(ms)이다.

 

◎ autoCommit (default: true) - auto-commit설정 

◎ connectionTimeout (default : 30000(30초))
 - 풀에서 커넥션을 얻어오기전까지 기다리는 최대 시간, 허용가능한 wait time을 초과시 SQLException이 발생한다.
 - 설정가능한 최소 값 : 250

◎ validationTimeout (default : 5000(5초))
 - valid 쿼리를 통해 커넥션이 유효한지 검사할 때 사용되는 timeout시간.(커넥션이 유효 검사 시 대기 시간을 지정)
 - 이 값은 connectionTimeout보다 작아야 한다.
 - 설정가능한 최소 값 : 250

◎ maximumPoolSize (default: 10)
 - 풀에 유지시킬 수 있는 최대 커넥션 수.
 - 풀의 커넥션 수가 옵션 값에 도달하게 되면 idle인 상태는 존재하지 않는다.
 - 풀이 이 크기에 도달하고 유휴 커넥션이 없을 때 connectionTimeout이 지날 때까지 getConnection() 호출은 블록킹된다.

◎ idleTimeout (default : 600000(10분))
 - pool에 일하지 않는 커넥션을 유지하는 시간.
 - 이 옵션은 minimumIdle이 maximumPoolSize보다 작게 설정되어 있을 때만 적용된다.
 - 이 옵션이 0이면 유휴 커넥션을 풀에서 제거하지 않는다.
 - 설정가능한 최소 값 : 10000(10초)

◎ minimumIdle (default: same as maximumPoolSize)
 - 유휴 커넥션의 최소 개수(아무런 일을 하지않아도 적어도 이 옵션 값의 size로 커넥션들을 유지해주는 설정이다.)
 - default값이 유동적이기 때문에 최적의 성능과 응답성을 생각하면 구지 설정하지 않는게 좋을 것 같다.

◎ maxLifetime (default : 1800000(30분))
 - 커넥션의 최대 유지 시간. 이 시간이 지난 커넥션 중에서 사용중인 커넥션은 종료된 이후에 풀에서 제거한다.
 - 갑자기 풀에서 많은 커넥션이 제거되는 것을 피하기 위해 negative attenuation을 적용해 점진적으로 제거한다.
 - 이 값이 0이면 풀에서 제거하지 않지만 idleTimeout은 적용된다.
 
◎ connectionTestQuery (default : 없음)
 - 커넥션이 유효한지 검사할 때 사용할 쿼리를 지정한다.(보통 SELECT 1 로 설정 한다.)
 - 드라이버가 JDBC4를 지원하면 이 프로퍼티를 설정하지 말자.(이 프로퍼티를 설정하지 않으면 JDBC4의 Conneciton.isValid()를 사용하여 유효성 검사 수행)
 - JDBC4 드라이버를 지원하지않는 환경에서 이 값을 설정하지 않는다면 error레벨 로그 리턴.

◎ leakDetectionThreshold
 - 커넥션이 누수 로그메시지가 나오기 전에 커넥션을 검사하여 pool에서 커넥션을 내보낼 수 있는 시간 설정.
 - 0으로 설정하면 누수 발견을 하지 않는다(leak detection 이용하지 않음). 허용하는 최소 값은 2000(2초)이다.
 - 설정가능한 최소 값 : 2000(2초)

 

 

[Spring] Spring Boot 시작하기 (7) - MyBatis에서의 DataBase 다중 연결

_overload 2020. 10. 8. 07:00
 

 

 

 

포스팅 시리즈

 

 

 

이번 포스팅에서는 Spring Boot에서 여러 개의 데이터베이스를 연결하는 방법에 대해 알아보겠습니다.

개인적으로 Spring Framework로 개발을 진행할 때 가장 스트레스로 다가오는 부분은 환경 설정인데요, 건드려야 할 부분도 많아 복잡하고 작업도 섬세하게 진행해야 해서 시간이 상당히 오래 걸리기 때문입니다.

그리고 한 번 설정 해 두면 웬만해서는 건드릴 부분이 많지 않기 때문에 금방 잊어버려서 새로운 프로젝트를 할 때마다 헤매는 것을 반복한 경험을 겪은 사람은 저뿐만은 아닐 것입니다.

 

설정과 관련한 부분을 최대한 간소화하고 자동화시킨 Spring Boot에서도 본 주제에 대한 작업은 꽤나 복잡한 편에 속하므로, 포스팅에서 작성하는 코드와 설정을 주의 깊게 봐주시길 바랍니다.

 

본 포스팅에서의 예제는 log4jdbc로 DB 로깅이 세팅되어있는 상태에서 진행되었으므로, logback 로깅 환경이 구축되어있지 않은 경우 예시에서의 application.properties 설정 구문이 약간 다르므로 참고해 주세요.

만약 본 예제와 환경을 맞추고 싶으신 분들은 아래의 링크를 참고해 주세요.

2020/10/06 - [Dev/Spring] - [Spring] Spring Boot 시작하기 (5) - log4jdbc를 이용한 Query로깅

 

[Spring] Spring Boot 시작하기 (5) - log4jdbc를 이용한 Query로깅

이번에는 저번 포스팅 끝에서 언급한 DB 요청과 응답에 대한 로깅 처리에 대해 다루겠습니다. 1. 의존성 주입 build.gradle 파일을 열고, DB 로그 기능을 사용하기 위해 log4jdbc 의존성을 등록합니다. #

dev-overload.tistory.com

 

 

본 포스팅을 정주행 하시는 분들을 위한 당부

 

1. DB 세팅

프로젝트 설정에 앞서, 연동할 데이터베이스를 먼저 설정해 주도록 하겠습니다.

본 예제에서는 데이터베이스를 2개를 사용합니다.

보통 하나 이상의 DB를 연결할 때 각 DB의 구분을 1개의 Master DB, n개의 Slave DB로 구분합니다.

 

아래의 표는 이번 예제에서 사용한 DB 정보입니다.

No. Master/slave Database System Database name IP Port
1 master MySQL MASTER 192.168.0.10 3306
2 slave mariadb SLAVE 192.168.0.13 3306

 

마스터로 사용할 데이터베이스는 윈도에 설치한 Mysql이며, 슬레이브 데이터베이스는 제가 사용하는 라즈베리파이에 설치된 mariadb입니다.

두 데이터베이스 모두 기본 포트를 사용했으며, 생성한 데이터베이스도 알기 쉽게 MASTER, SLAVE로 생성했습니다.

 

각 데이터베이스는 외부에서 접속할 수 있는 환경을 마련해 두어야 합니다.

DB 외부 사용에 대한 정보는 아래 링크를 참고해 주세요.

zetawiki.com/wiki/MySQL_%EC%9B%90%EA%B2%A9_%EC%A0%91%EC%86%8D_%ED%97%88%EC%9A%A9

 

MySQL 원격 접속 허용 - 제타위키

다음 문자열 포함...

zetawiki.com

 

 

마스터와 슬레이브 DB에 아래의 쿼리문을 사용해 테이블 생성 및 테스트 데이터를 세팅해 주겠습니다.

구분의 용이를 위해 마스터 DB의 테이블은 SALARY, 슬레이브 DB는 COUNTRY로 종류를 구분해 주었습니다.

 

Master DataBase

# Master DataBase
mysql> CREATE DATABASE MASTER;
mysql> USE MASTER;
mysql> CREATE TABLE SALARY(ID INT NOT NULL PRIMARY KEY, NAME CHAR(10), EMAIL CHAR(20) NOT NULL);

mysql> INSERT INTO SALARY VALUES (1,'AAA','AAA@example.com');
mysql> INSERT INTO SALARY VALUES (2,'BBB','BBB@example.com');
mysql> FLUSH PRIVILEGES;

 

Master DB

위 사진과 같이 테이블이 세팅되었는지 확인합니다.

 

Slave DataBase

# Slave DataBase
mysql> CREATE DATABASE SLAVE;
mysql> USE SLAVE;
mysql> CREATE TABLE COUNTRY(ID INT NOT NULL PRIMARY KEY, CONTINENT CHAR(20), COUNTRY CHAR(20) NOT NULL);

mysql> INSERT INTO COUNTRY VALUES (1,'Asia','Korea');
mysql> INSERT INTO COUNTRY VALUES (2,'Asia','Japan');
mysql> FLUSH PRIVILEGES;

 

Slave DB

슬레이브 DB도 마찬가지로 적용 여부를 확인합니다.

 

2. application.properties 설정

생성한 데이터베이스에 대한 정보를 Spring Boot의 application.properties 파일에 아래와 같이 작성합니다.

# MASTER DB
spring.master.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.master.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.10:3306/MASTER?characterEncoding=UTF-8&serverTimezone=UTC
spring.master.datasource.username={user name}
spring.master.datasource.password={password}
spring.master.datasource.connection-test-query=SELECT 1

# SLAVE 1 DB
spring.slave-1.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.slave-1.datasource.jdbc-url=jdbc:log4jdbc:mysql://192.168.0.13:3306/SLAVE?characterEncoding=UTF-8&serverTimezone=UTC
spring.slave-1.datasource.username={user name}
spring.slave-1.datasource.password={password}
spring.slave-1.datasource.connection-test-query=SELECT 1

{user name}, {password} 부분은 각자 개인의 데이터베이스 계정 정보를 의미합니다.

 

슬레이브 DB 설정에서 slave-1이라는 이름으로 설정값을 주었는데, 이는 슬레이브 DB는 얼마든지 여러 개가 올 수 있고, 나중에라도 추가가 될 수 있는 DB이기 때문에 그때를 고려한 넘버링입니다. (ex slave-1, slave-2, slave-3)

 

이때, STS 환경이라면 DB 설정 정보에 노란색 밑줄로 정의되지 않은 이름이라고 경고를 띄워줄텐데, 올바르게 구성한 정보이므로 무시합니다.

 

3. Sql 세션 구성 정보 작성

이제, 각 데이터베이스의 설정 정보를 토대로 SqlSession을 생성하고 트랜젝션이 이루어지도록 구성 설정을 해 주어야 합니다.

간단하게 생각하면 application.properties에서 정의한 DB 연결 정보를 적용하고 그 DB와 상호작용하는 mapper 파일을 정의하는 내용을 작성하는 것입니다.

 

위 사진과 같이 com.example.demo 경로에 config라는 패키지를 생성하고 안에 각각 MasterDataBaseConfig.java, Slave1DataBaseConfig.java 파일을 생성합니다.

 

그리고 각 파일에 아래와 같이 작성합니다.

 

MasterDataBase.Config.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(value="com.example.demo.mapper.master", sqlSessionFactoryRef="masterSqlSessionFactory")
@EnableTransactionManagement
public class MasterDataBaseConfig {
	
	@Primary
	@Bean(name="masterDataSource")
	@ConfigurationProperties(prefix="spring.master.datasource")
	public DataSource masterDataSource() {
		//application.properties에서 정의한 DB 연결 정보를 빌드
		return DataSourceBuilder.create().build();
	}
	
	@Primary
	@Bean(name="masterSqlSessionFactory")
	public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource, ApplicationContext applicationContext) throws Exception{
		//세션 생성 시, 빌드된 DataSource를 세팅하고 SQL문을 관리할 mapper.xml의 경로를 알려준다.
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(masterDataSource);
		sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:com/example/demo/mybatis/master/*.xml"));
		return sqlSessionFactoryBean.getObject();
	}
	
	@Primary
	@Bean(name="masterSqlSessionTemplate")
	public SqlSessionTemplate masterSqlSessionTemplate(SqlSessionFactory masterSqlSessionFactory) throws Exception{
		return new SqlSessionTemplate(masterSqlSessionFactory);
	}
	
}

 

Slave1DataBaseConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@MapperScan(value="com.example.demo.mapper.slave1", sqlSessionFactoryRef="slave1SqlSessionFactory")
@EnableTransactionManagement
public class Slave1DataBaseConfig {
	
	@Bean(name="slave1DataSource")
	@ConfigurationProperties(prefix="spring.slave-1.datasource")
	public DataSource masterDataSource() {
		//application.properties에서 정의한 DB 연결 정보를 빌드
		return DataSourceBuilder.create().build();
	}
	
	@Bean(name="slave1SqlSessionFactory")
	public SqlSessionFactory slave1SqlSessionFactory(@Qualifier("slave1DataSource") DataSource slave1DataSource, ApplicationContext applicationContext) throws Exception{
		//세션 생성 시, 빌드된 DataSource를 세팅하고 SQL문을 관리할 mapper.xml의 경로를 알려준다.
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(slave1DataSource);
		sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:com/example/demo/mybatis/slave1/*.xml"));
		return sqlSessionFactoryBean.getObject();
	}
	
	@Bean(name="slave1SqlSessionTemplate")
	public SqlSessionTemplate slave1SqlSessionTemplate(SqlSessionFactory slave1SqlSessionFactory) throws Exception{
		return new SqlSessionTemplate(slave1SqlSessionFactory);
	}
	
}

 

 

import 된 객체들을 확인하고 적용해 줍니다.

그리고 각 어노테이션에서 정의한 Ref와 name 정보를 꼼꼼하게 확인해 주어야 합니다.

대부분의 에러의 원인은 이 Config 파일에서 발생합니다.

 

MasterDataBaseConfig와 내용이 겹치면 안 되며, 각 어노테이션에서 참조하는 name과 각 메서드의 이름이 일치하는지 잘 확인합니다.

 

사실 이 작업이 완료되었다면 본 포스팅에서 목적한 바의 절반은 완료된 것입니다.

이제는 이 구성 스크립트에서 정의한 대로 올바른 경로에 올바른 이름으로 나머지 필요 파일들을 생성해 주어야 합니다.

 

4. 각 테이블에 대응할 Model 객체 정의

데이터베이스의 테이블 정보를 받을 수 있는 Model 객체를 정의하겠습니다.

com.example.demo 패키지에 model 패키지를 생성합니다.

그리고 마스터 DB의 Salary 테이블 정보를 받을 SalaryModel.java와 슬레이브 DB의 Country 테이블을 받을 CountryModel.java 파일을 생성하고 아래와 같이 정의합니다.

본 예제에서는 lombok을 사용했는데, 만약 lombok을 사용하지 않는 환경이라면 getter/setter 메서드를 각각 정의해줍니다.

 

SalaryModel.java - master

//SalaryModel.java

package com.example.demo.model;

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;

@Builder
@Data
public class SalaryModel {
	private int id;
	@NonNull @Builder.Default private String name = "NULL NAME";
	@NonNull @Builder.Default private String email = "NULL EMAIL";
}

 

CountryModel.java - slave 1

// CountryModel.java

package com.example.demo.model;

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;

@Builder @Data
public class CountryModel {
	private int id;
	@NonNull @Builder.Default private String continent = "No CONTINENT";
	@NonNull @Builder.Default private String country = "No Country";
}

 

 

5. Mapper 인터페이스 정의

이제, 쿼리문 요청하기 직전 단계인 Mapper 인터페이스를 정의합니다.

 

Mapper 인터페이스는 Config.java에서 MapperScan 어노테이션을 통해 각자 별도의 패키지에서 탐색하도록 정의했으므로 패키지를 각각 생성해야 합니다.

 

 

com.example.demo에 공통으로 사용할 mapper 패키지를 생성하고 그 안에 master, slave1 패키지를 추가로 생성합니다.

그리고 master 패키지 안에는 MasterDataBaseMapper.java 파일을, slave1 패키지 안에는 Slave1DataBaseMapper.java 파일을 생성합니다.

 

그리고 각 파일에 아래와 같이 내용을 작성합니다.

 

MasterDataBaseMapper.java

// MasterDataBaseMapper.java

package com.example.demo.mapper.master;

import java.util.List;

import com.example.demo.model.SalaryModel;

public interface MasterDataBaseMapper {
	public List<SalaryModel> getSalary() throws Exception;
}

 

Slave1DataBaseMapper.java

// Slave1DataBaseMapper.java

package com.example.demo.mapper.slave1;

import java.util.List;

import com.example.demo.model.CountryModel;

public interface Slave1DataBaseMapper {
	public List<CountryModel> getCountry() throws Exception;
}

 

위 파일들은 config.java에서 @MapperScan을 통해 mapper 파일의 위치를 알려주었으므로 @Repository @Mapper 어노테이션을 붙여줄 필요가 없습니다.

 

각 mapper 파일에는 DB에 있는 정보들을 조회하는 간단한 메서드만 정의해 주었습니다.

 

6. 각 테이블의 Mapper xml파일 정의

이제, 각 테이블에 요청할 쿼리문을 관리할 xml 파일을 생성해 주어야 합니다.

이 xml 파일도 데이터베이스 별로 패키지를 나누어 줍니다.

 

거듭 언급하지만 생성하는 패키지 경로는 confg.java에서 정의한 대로 생성해야 합니다.

mapper 인터페이스와 구분을 주기 위해 xml 파일들의 패키지는 mybatis를 상위 패키지로, master, slave1 패키지를 하위로 구성하고 master에는 MasterDataBaseMapper.xml 파일을, slave1에는 Slave1DataBaseMapper.xml을 생성합니다.

그리고 아래와 같이 각 파일에 작성합니다.

 

MasterDataBaseMapper.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">

<mapper namespace="com.example.demo.mapper.master.MasterDataBaseMapper">
	<select id="getSalary" resultType="com.example.demo.model.SalaryModel">
		SELECT * FROM SALARY;
	</select>
</mapper>

 

Slave1DataBaseMapper.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">

<mapper namespace="com.example.demo.mapper.slave1.Slave1DataBaseMapper">
	<select id="getCountry" resultType="com.example.demo.model.CountryModel">
		SELECT * FROM COUNTRY;
	</select>
</mapper>

 

7. 사용자 클래스에서 접근하기 위한 Service 정의

service 패키지를 생성하고 MasterDataBaseService.java와 Slave1DataBaseService.java 스크립트를 생성합니다.

위 두 파일은 클래스를 별도로 두어도 되고 같은 패키지에서 관리해도 무방합니다.

예제에서는 service 패키지에서 같이 관리하도록 하겠습니다.

아래의 내용을 작성합니다.

 

MasterDataBaseService.java

// MasterDataBaseService.java

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.mapper.master.MasterDataBaseMapper;
import com.example.demo.model.SalaryModel;

@Service
public class MasterDataBaseService {
	@Autowired
	MasterDataBaseMapper masterDataBaseMapper;
	
	public List<SalaryModel> getSalary() throws Exception{
		return masterDataBaseMapper.getSalary();
	}
	
}

 

Slave1DataBaseService.java

// Slave1DataBaseService.java

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.mapper.slave1.Slave1DataBaseMapper;
import com.example.demo.model.CountryModel;

@Service
public class Slave1DataBaseService {
	@Autowired
	Slave1DataBaseMapper slave1DataBaseMapper;
	
	public List<CountryModel> getCountry() throws Exception {
		return slave1DataBaseMapper.getCountry();
	}
	
}

 

8. Controller에서 Service 호출

@Controller
public class HomeController {
	@Autowired
	private MasterDataBaseService masterDataBaseService;
	
	@Autowired
	private Slave1DataBaseService slave1DataBaseService;
	
	@RequestMapping(value = "/home", method = RequestMethod.GET)
	public ModelAndView goHome(HttpServletRequest request) throws Exception {
		
		ModelAndView mav = new ModelAndView();
		
		List<SalaryModel> salaryList = masterDataBaseService.getSalary();
		List<CountryModel> countryList = slave1DataBaseService.getCountry();
		
		mav.addObject("salaryList", salaryList);
		mav.addObject("countryList",countryList);
		mav.setViewName("content/home.html");
		return mav;
	}
	
}

 

본 예제에서는 /home 주소로 접속했을 때, 각기 다른 DB 테이블의 정보를 요청 해 페이지에 출력하도록 구성했습니다.

Service 스크립트는 Bean 객체이기 때문에 반드시 @Autowired 어노테이션으로 주입해야 합니다.

 

9. View 페이지에 파싱

컨트롤러에서 전달한 데이터를 확인하기 위해 아래와 같이 적절히 태그를 작성합니다.

아래의 스크립트는 thymeleaf 템플릿에 대한 예제이며, JSP를 이용하시는 분들은 JSTL 태그로 적절히 작성해 주세요.

<!-- home.html -->
<h3>Master DB in Salary Table</h3>
<table border="1">
    <tr>
        <th>id</th>
        <th>name</th>
        <th>email</th>
    </tr>
    <th:block th:each="salary : ${salaryList}">
    <tr>
        <td th:text="${ salary.id }"></td>
        <td th:text="${ salary.name }"></td>
        <td th:text="${ salary.email }"></td>
    </tr>
    </th:block>
</table>

<hr>

<h3>Slave1 DB in Country Table</h3>
<table border="1">
    <tr>
        <th>id</th>
        <th>continent</th>
        <th>country</th>
    </tr>
    <th:block th:each="country : ${countryList}">
    <tr>
        <td th:text="${ country.id }"></td>
        <td th:text="${ country.continent }"></td>
        <td th:text="${ country.country }"></td>
    </tr>
    </th:block>
</table>

 

이제 컨트롤러에서 정의한 주소로 접속해 봅니다.

 

만약 log4jdbc를 적용한 프로젝트라면 아래와 같이 데이터 로그를 출력해 줄 것입니다.

 

 

로그에서 각기 다른 두 개의 테이블 정보를 보여주고 있습니다.

 

 

페이지에서도 정상적으로 데이터를 출력하고 있음을 확인했습니다.

 

개인적으로는 아직까지는 이렇게 하나의 애플리케이션에서 여러 개의 데이터 베이스를 동시에 이용하는 프로젝트를 경험해보지 못했습니다.

하지만, 프로젝트의 성격에 따라 특히나 Spring과 같은 엔터프라이즈 프레임워크에서는 이와 같은 데이터 베이스 다중 연동 기능이 필요한 순간은 반드시 올 수 있으므로 지금 당장은 활용하지 못하더라도 한번쯤은 구현해 보는 것도 나쁘지 않은 시스템인 것 같습니다.

 

이것으로 Mybatis에서의 DataBase 다중 연결 포스팅을 마치겠습니다.

 

 

3. 웹개발/3_1_3 스프링부트

[스프링부트 (3)] SpringMVC(2) Spring Boot View 설정 및 JSP 연동하기(Thymeleaf 추가)

by 갓대희 2020. 2. 11.
 

[스프링부트 (3)] SpringMVC(2) Spring Boot View 설정 및 JSP 연동하기(Thymeleaf 추가)

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ 스프링 부트 View 설정방법 ] 입니다. : ) 

 

 

 

이번 포스팅에선 MVC 패턴중 View 설정하는 방법을 설명하려 한다. 그리고 JSP 및 VelocityThymeleaf를 예제를 포함 하려 한다.

 

0. 들어가기 앞서

▶ JSP 제한 사항

 - 스프링 공식문서에 보면 내장된 서블릿 컨테이너에는 jsp 제한사항이 있다.

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-developing-web-applications
 - 스프링 부트는 가능하다면 jsp를 피하고 Thymeleaf와 같은 템플릿 엔진을 사용하라고 권장한다.

 

7.1.10. Template Engines

As well as REST web services, you can also use Spring MVC to serve dynamic HTML content. 
Spring MVC supports a variety of templating technologies, including Thymeleaf, FreeMarker, and JSPs. 
Also, many other templating engines include their own Spring MVC integrations.

Spring Boot includes auto-configuration support for the following templating engines:
FreeMarker, Groovy, Thymeleaf, Mustache

If possible, JSPs should be avoided. There are several known limitations when using them with embedded servlet containers.

7.4.5. JSP Limitations
When running a Spring Boot application that uses an embedded servlet container 
(and is packaged as an executable archive), there are some limitations in the JSP support.

With Jetty and Tomcat, it should work if you use war packaging. 
An executable war will work when launched with java -jar, and will also be deployable to any standard container. 
JSPs are not supported when using an executable jar.
Undertow does not support JSPs.
Creating a custom error.jsp page does not override the default view for error handling. Custom error pages should be used instead.

 

 

1. Spring Boot에서의 View 

▶ 스프링 부트에서도 여러가지 뷰를 사용 할 수 있다.

ㆍJSP/JSTL
ㆍThymeleaf
ㆍFreeMarker
ㆍVelocity
ㆍGroovy Template Engine
ㆍTiles

... etc

 

※ 앞서 포스팅에서 기본 프로젝트 구조를 잡았을 것이다
src > main > reousrce > [static] 폴더엔 정적 리소스들을 추가하였을 것이다.
src > main > reousrce > [templates] 폴더도 확인할 수 있을 것인데 Thymeleaf(.html), Velocity(.vm)등과 관련된 파일만 동작하고 jsp 파일은 추가하여도 작동하지 않으니 참고 하자.

 

※ 폴더 구조

src
└─ main
   └─ resource
      └─ templates (View: Thymeleaf, Groovy, Velocity 등)
      └─ static    (정적 컨텐츠 : html, css, js, image 등)

 

▶ 내장 Tomcat

 - 스프링부트는 웹 개발을 위해 자주 사용되는 Spring의 Component들과 Tomcat, Jetty 등의 경량 웹 어플리케이션 서버를 통합한 경량의 웹개발 프레임 워크이다.

 - 즉 별도의 웹 어플리케이션 서버 없이 SpringBoot를 통해 프레임워크와 웹 어플리케이션 서버를 통합했다고 생각하면 된다.

 

앞서 포스팅에서 다음 디펜던시(spring-boot-starter-web)를 추가 하였을 것이다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-web 에는 tomcat이 포함되어 있지만, JSP 엔진은 포함하고 있지 않다.
하지만 간단한 설정만 해주면 JSP view를 사용 가능하다.

이제부터 jsp를 사용하는 방법을 알아 보도록 하자.

 

"JSP"
2. pom.xml / gradle 설정 

▶ build.gradle 인 경우

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl:1.2'

compile('org.apache.tomcat.embed:tomcat-embed-jasper')
compile('javax.servlet:jstl:1.2')

 

▶ pom.xml 인 경우

 - jasper,jstl을 의존성에 추가해야 JSP파일의 구동이 가능하다.
   (앞서 말했듯이 jsp 파일은 Springboot의 templates 폴더안에서 작동하지 않으니 참고하자.)

<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
	<scope>provided</scope>
</dependency>
        
<!-- jstl 라이브러리 -->
<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>jstl</artifactId>
</dependency>

 

3. JSP  경로 설정(디렉토리 생성)

▶ 스프링 부트에서도 여러가지 뷰를 사용 할 수 있다.

 - WEB-INF/jsp/(또는 WEB-INF/views/)

   (full 경로를 말씀해달라고 하시는 분들이 있어 추가하자면 다음과 같습니다.

    /src/main/webapp/WEB-INF/jsp/)
 - 톰캣기반 자바 웹어플리케이션에서는 보안상 jsp 위치를 URL로 직접접근할 수 없는 WEB-INF폴더 아래 위치시킨다.

 

ex) test.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>View Test Page</title>
</head>
<body>
	<h2>Hello! ${name}</h2>
	<div>JSP List Test</div>
        <c:forEach var="item" items="${list}" varStatus="idx">
        ${idx.index}, ${item} <br />
        </c:forEach>
</body>
</html>

 

 

4. application.properties

※ Spring 애플리케이션 시작시 application.properties 파일에 정의된 내용을 로드한다.
   (스프링부트의 AutoConfiguration을 통해 자동 설정한 속성값들이 존재하며, application.properties의 해당 값들은 오버라이드 한다.)

▶ server.port

 - 별다른 설정을 하지 않으면 default 포트는 8080이다.
 - Spring Boot에 기본적으로 내장되어있는 Tomcat과 Jetty와 같은 WAS의 포트번호를 임의로 변경 할 수 있다.

server.port = 8888

▶ prefix/suffix

 - jsp 페이지를 처리하기 위한 prefix와 suffix를 application.properties에 추가 하자.
 - 앞서 생성한 JSP 경로를 prefix로 선언, 그리고 확장자럴 suffix로 선언할 수 있다.

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

 

 

5. Controller 설정
@RequestMapping("/test")
public ModelAndView test() throws Exception{
	ModelAndView mav = new ModelAndView("test");
	mav.addObject("name", "goddaehee");

	List<String> testList = new ArrayList<String>();
	testList.add("a");
	testList.add("b");
	testList.add("c");

	mav.addObject("list", testList);
	return mav;
}

 

 

6. jsp파일 서버 재시작 없이 바로 적용하기

 - 스프링 부트는 스프링 프로젝트와 다르게, 동적 파일들의 파일 변경을 자동으로 반영하지 않는다.
 - 기존 스프링과 동일하게 자동 반영하기 위해선 다음과 같은 설정을 추가하여 주면 된다.
 - Spring Boot 2.x 버전 기준으로 포스팅 한다.
 

▶ build.gradle 인 경우

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools
implementation group: 'org.springframework.boot', name: 'spring-boot-devtools', version: '2.7.1'

 

▶ pom.xml 인 경우

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
</dependency>

 

▶ application.properties

devtools.livereload.enabled=true

 

※ 참고 Spring Boot 1.x 버전 기준으로는 application.properties에 다음 내용을 추가해주면 적용된다.

server.jsp-servlet.init-parameters.development=true 

 

◎ Application을 실행하여 보자. 다음과 같은 화면을 볼 수 있다.

 

 

"thymeleaf"
7. thymeleaf

Thymeleaf 홈페이지 
https://www.thymeleaf.org/index.html

Thymeleaf + Spring
 - https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

 

▶ Dependency 추가

 - build.gradle

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

 

 - pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

 

▶ 파일 기본 경로 및 html 생성

 - 경로 : /src/main/resources/templates/thymeleaf

 - thymeleafTest.html

<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello thymeleaf</title>
</head>
<body>
	<h1>Hello thymeleaf</h1>
	<h2>name = <span th:text="${testModel.name}"></span></h2>
	<h3>id =  <span th:text="${testModel.id}"></span></h3>
</body>
</html>

▶ application.properties 설정 

#JSP와 같이 사용할 경우 뷰 구분을 위해 컨트롤러가 뷰 이름을 반환할때 thymeleaf/ 로 시작하면 타임리프로 처리하도록 view-names 지정
spring.thymeleaf.view-names=thymeleaf/*
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
#thymeleaf를 사용하다 수정 사항이 생길 때 수정을 하면 재시작을 해줘야 한다. 이를 무시하고 브라우저 새로고침시 수정사항 반영을 취해 cache=false 설정(운영시는 true)
spring.thymeleaf.cache=false
spring.thymeleaf.check-template-location=true

 Vo 설정 

package com.god.bo.test.vo;

public class TestVo {
    private Long mbrNo;
    private String id;
    private String name;

    public TestVo() {
    }

    public TestVo(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getMbrNo() {
        return mbrNo;
    }

    public void setMbrNo(Long mbrNo) {
        this.mbrNo = mbrNo;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 Controller 설정 

@RequestMapping("/thymeleafTest")
public String thymeleafTest(Model model) {
    TestVo testModel = new TestVo("goddaehee", "갓대희") ;
    model.addAttribute("testModel", testModel);
    return "thymeleaf/thymeleafTest";
}

 

 

스프링부트를 이용해 아주 간단히 View 설정해 보았다.

[스프링부트 (2)] SpringMVC(1) Controller 생성하기

by 갓대희 2020. 2. 4.
 

[스프링부트 (2)] SpringMVC(1) Controller 생성하기

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ 스프링 부트 Controller  ] 입니다. : ) 

 

 

 

아주 간단히 Controller에 대해 정리도 하려고 하니, 실제 스프링 부트의 Controller생성 방법으로 얼른 스킵하여 보셔도 무방할 듯하다.

1. MVC

▶ 개요

 - MVC(Model View Controller)란 하나의 디자인 패턴이다.

 - 스프링 MVC : 스프링이 제공하는 웹 어플리케이션 구축 전용 MVC 프레임워크.

 

1. 모델(Model) : 비즈니스 규칙을 표현 

2. 뷰(View) : 프레젠테이션을 표현

3. 컨트롤러(Controller) : 위 두가지를 분리하기 위하여 양측 사이에 배치된 인터페이스

 

이번 포스팅은 MVC의 개념을 설명하는 포스팅은 아니기에 이정도만 작성하고, 그 첫번째로 스프링 부트에서 Controller 생성 하는 방법을 알아보려 한다.

 

 

2. Controller

▶ Controller란?

 - Controller에 대해 간단히 말하자면 MVC에서 C에 해당 하며 주로 사용자의 요청을 처리 한 후 지정된 뷰에 모델 객체를 넘겨주는 역할을 한다.

 

▶ Controller 관련 대표적인 Annotation을 알아보자

1)  @Controller

 - Controller의 역할을 수행 한다고 명시(해당 클래스를 Controller로 사용한다고 Spring FrameWork에 알린다.)

   필요한 비즈니스 로직을 호출하여 전달할 모델(Model)과 이동할 뷰(View) 정보를 DispatherServlet에 반환 한다.

 - Bean으로 등록

 - @Component의 구체화 된 어노테이션

 

2)  @RequestMapping

 - 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지 맵핑하기 위한 어노테이션

 - 클래스나 메서드 선언부에 @RequestMapping과 함께 URL을 명시하여 사용한다.

 -  viewName 생략시 @RequestMapping의 path로 설정한 URL이 default viewName

 

RequestMapping 속성들

1) value(String[])  : URL 값

/* EX) */
@RequestMapping(value="/login")
@RequestMapping("/login") 

 

2) method (RequestMethod[]) : HTTP Request 메소드 값

 - GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE

/* EX1) */
@RequestMapping(value="/login", method=@RequestMethod.GET)
/* EX2) */
@RequestMapping(value="/login", method=@RequestMethod.POST)

※ Spring4.3 이후 ex1), ex2)는 다음과 같이 쓸수 있다.

ex1) @GetMapping("/login")

    == @RequestMapping("/login", method=@RequestMethod.GET)

ex2) @PostMapping("/login")

   == @RequestMapping("/login", method=@RequestMethod.POST)

 

3) params(String[]) : HTTP Request 파라미터

 @RequestParam : 사용자가 원하는 매개변수에 값을 매핑하기위해 사용한다.

/* EX1) */
@PostMapping("/member")
public String member(@RequestParam String name, @RequestParam Int age)

여기서 @ReauestParam은 생략 가능하다. 사용자가 입력한 key값과 매개변수의 이름을 비교하여 값을 넣어주기 때문이다. 결국 다음의 ex2)와 ex1)은 동일한의미다.

/* EX2) */
@PostMapping("/member")
public String member(String name, Int age)

 

② @PathVariable : url 경로를 변수화하여 사용할 수 있도록 해준다.

@RequestMapping("/member/{name}/{age}")
public String member(@PathVariable("name") String name, @PathVariable("age") String age)

=> RequestMapping의 {name}과 PathVariable 의 String name을 매핑 하여 준다.

 

그리고 잘 사용해보진 않지만 다음과 같은 속성도 존재 한다.

 

4) consumes(String[]) : Request Body에 담는 타입을 제한할 수 있다.

ex) @PostMapping("/login", consumes="application/json")

헤더에 application/json이 존재 해야 처리한다.

 

 

3. 스프링 부트 Controller 생성

이제부터 Test용 Controller 생성하여 서버를 띄워보도록 하자.

(이전 포스팅 까지 하여 Application, index.html을 생성 해두었다.)

▶ TestController 생성 (@Controller)

 - 나와같은 경우는 일단 다음과 같이 테스트용으로 패키지 구조를 잡았다.

 - 원하는 패키지에 Controller.java파일을 생성해보자.

 - 먼저 TestController.java 파일을 생성하여 다음과 같이 작성 하였다.

package com.god.bo.test.controller;

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

@Controller
public class TestController {

    @RequestMapping(value = "/home")
    public String home(){

        return "index.html";
    }

}

 

▶ View 생성 (INDEX 페이지 만들기)

 - 이미 전 포스팅에서 생성 완료 하였지만 다시 한번 index.html 생성 해보자. (JSP는 다음포스팅에)

 - src/main/resources/static 경로에 index.html 파일을 추가하고 다음 소스와 같이 코딩 하자.

(static 폴더는 정적 html 문서, 이미지, 영상 등 관리)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    <h1>Hello World!</h1>
</body>
</html>

 

▶ 실행하기

 - 이미 생성 해 두었던 Application 파일을 실행 시키고 설정한 URL로 접속해보자. 

 - 파일에서 우클릭 > Run '~~Aplication.main()' 클릭

 - 설정한 URL로 index.html에 접근한 가능한 것을 확인할 수 있다.

 - 추후 포스팅 예정이지만 내장 톰캣 대신 Jetty등 외부 서버를 사용할 수도 있다.

 - 혹시 이미 8080포트를 사용하고 있는 경우엔 포트 변경 하자.

/src/main/resources/application.properties 에 다음과 같이 작성하여 포트 변경한다. (ex 8000)

server.port = 8000

 

 @ResponseBody 로 객체 정보 전달 하기

 - view가 아닌 data를 반환해야 하는 경우라면 @ResponseBody를 사용하면 된다.

 - @ResponseBody 어노테이션을 통해 간단하게는 String, Map, JSON 등을 전달할 수 있고 다음 예제를 통해 간단한 Ajax 통신을 해보자.

package com.god.bo.test.controller;

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

@Controller
public class TestController {
    @RequestMapping(value = "/home")
    public String home(){
        return "index.html";
    }

    @ResponseBody
    @RequestMapping("/valueTest")
    public String valueTest(){
        String value = "테스트 String";
        return value;
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        $.ajax({
            type: "GET",
            url: "/valueTest",
            success: (data) => {
            console.log(data);
            $('#contents').html(data);
            }
        });
    </script>
</head>
<body>
    <h1>Hello World!</h1>
    <div id="contents">
    </div>
</body>
</html>

 - 서버 재기동시 다음과 같이 값을 전달 받은 것을 확인 할 수 있다.

 @RestController 생성

 - Spring 4.0이상은 @Controller와 @ResponseBody 어노테이션을 추가하는 것 대신 @RestController을 제공하다.

 - @RestController를 사용하면 @ResponseBody를 추가 할 필요가 없고, @ResponseBody 어노테이션은 기본적으로 활성화되어 있다.

package com.god.bo.test.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;9

@RestController
public class TestRestController {

    @RequestMapping(value="/testValue", method = RequestMethod.GET)
    public String getTestValue(){
        String TestValue = "레스트컨트롤러 테스트";
        return TestValue;
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script>
        $.ajax({
            type: "GET",
            url: "/testValue",
            success: (data) => {
            console.log(data);
            $('#contents').html(data);
            }
        });
    </script>
</head>
<body>
    <h1>Hello World!</h1>
    <div id="contents">
    </div>
</body>
</html>

 - 서버 재기동 해보자. @Controller + @ResponseBody와 동일한 역할을 하는 것을 확인 할 수 있다.

 - @RestController를 디컴파일 해보면 @Controller와 @ResponseBody를 포함 하고 있는 것을 볼 수 있고 이때문에 위와 같은 기능을 사용할 수 있는 것이다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

스프링부트를 이용해 아주 간단히 Controller 설정해 보았다.

 

3. 웹개발/3_1_3 스프링부트

[스프링부트 (1)] 스프링부트 시작하기 (SpringBoot 프로젝트 설정 방법)

by 갓대희 2020. 4. 9.
 

[스프링부트 (1)] 스프링부트 시작하기 (SpringBoot 프로젝트 설정 방법)

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ 스프링 부트 기초. 스프링 부트란? 그리고 스프링 부트 프로젝트 생성 방법 ] 입니다. : ) 

 

스프링 부트가 나온지 어느덧 꽤 많은 시간이 흐른것 같다. 계속 해오던 쇼핑몰 운영 업무를 해오다 보니 스프링 부트를 제대로 사용할 일이 없어, 이번 잠시 주어진 휴식 기간에 스프링 부트에 대한 내용을 정리해 보려 한다.

운영 업무를 하다보면 쓰던 기술, 해오던 업무에 익숙하고 무언가의 불안함이 생긴다. 다시 SI를 하고 싶은 마음도 간간히 생기기도 하고.. 주저리 그만하고 스프링 부트에 대해 알아보자.

1. SpringBoot 기초

▶ 스프링 부트란?

 - 스프링 프레임워크를 사용하는 프로젝트를 아주 간편하게 설정할 수 있는 스프링 프레임웍의 서브 프로젝트라고 할 수 있다.

 

 - Spring Boot makes it easy to create stand-alone.

   단독실행가능한 스프링애플리케이션을 생성한다. 

 

 - Most Spring Boot applications need very little Spring configuration.

   Spring Boot는 최소한의 초기 스프링 구성으로 가능한 한 빨리 시작하고 실행할 수 있도록 설계되었다.

 

 - 웹 컨테이너를 내장하고 있어 최소한의 설정으로 쉽게 웹 어플리케이션을 만들 수 있다.

 

▶ 스프링 부트를 왜 사용해야 하나? 장점?

 스프링 프레임 워크와 비교하여 여러 가지 장점이 있다.

 

 - 스프링 프레임 워크를 사용 하면 많은 XML 설정파일등을 작성 하는 등 설정하는 방법이 어려운 편이기 때문에 보통 검색을 통해 설정 내용을 복사 하거나, 기존 설정파일들을 복붙하기 일수였다.

 하지만 스프링 부트는 반복되는 개발환경 구축을 위한 코드작성등의 노력을 줄여주고 쉽고 빠르게 프로젝트를 설정할 수 있도록 도와준다.

 

 - 매우 빠르게 모든 스프링 개발에 관한 경험에 광범위한 접근을 제공한다.

 

 - 프로젝트 환경 구축에서 큰 영역을 차지하는 비기능적인 기능들을 기본적으로 제공한다.

   (내장형 서버, 시큐리티, 측정, 상태 점검, 외부 설정)

 

등등 다양한 장점을 갖고 있지만, 무엇보다 스프링 부트를 직접 사용해보고 체감해보는 것이 빠를 것 같다. 그럼 이제 직접 스프링 부트 프로젝트를 생성해 보자.

 

2. 스프링 부트 시작하기

▶ 1. 스프링 이니셜라이즈로 SpringBoot 프로젝트 쉽게 생성 하기

 - 요즘은 이클립스, 인텔리 제이 등을 통해 쉽게 스프링, 스프링 부트 프로젝트를 시작할 수 있기도 하다.

 - 스프링 이니셜라이즈로 쉽게 스프링 부트 프로젝트를 생성 해보자.

 - 해당 싸이트는 개속 버전업, 리뉴얼 되고 있지만, 얼추 큰 틀은 같다.

 

1.1 스프링 이니셜라이즈 사이트 접속

 - https://start.spring.io/

 - 다음 그림을 참고하여 설정 할 예정이다. 자세한 내용은 그림 하단 내용 참고.


 - 현재 2023. 2월 설정 시 initializr - 3.0.2

 - Spring boot 3.0부터는 Java 17부터 지원한다.

 - 예전 spring boot 2.x 대 / Maven으로 설정한 내용은 참고로 제일 하단에 기록으로 남겨 두도록 한다.

 - 프로젝트, GroupId, Artifact ID, 패킹, 자바 버전 등을 설정 하여 주자.

 - Dependencies는 Spring Web 을 추가 해주도록 한다. (지금 설정하지 않더라도 build.gradle 에서 설정해주면 된다.)

 

 GroupId  GroupId는 자신의 프로젝트를 식별해주는 고유 아이디.

                  'com.godDaeHee.web' 과 같이 인터넷 주소를 뒤집어 써놓은 형태.

★ ArtifactId : ArtifactId는 버전 정보를 생략한 이름(jar).

                  보통 프로젝트 ID와 동일하게 작성한다.

나는 'com.god.bo'라는 GroupId를 사용할 예정이다. GroupId는 'com.god', ArtifactId는 'bo'로 설정 해보도록 하겠다.

 

Generate - Ctrl + Enter 를 클릭하면 프로젝트 파일이 압축되어 다운로드 된다.

이를 압축 해제 하도록 하자.

 

1.2 프로젝트 Import

 - 이클립스나 인텔리제이를 통해 압축 해제한 프로젝틀르 임폴드 해준다.

 - 이니셜 라이저를 통해 프로젝트 생성이 완료 되었다. 

 

▶ 2. build.gradle 설정하기

★ 2.1 spring-boot-starter-web

 -  Spring MVC를 사용한 RESTful서비스를 개발하는데 사용.

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

 

★ 2.2 spring-boot-starter-test

 - Junit, Hamcrest, Mockito를 포함하는 스프링 어플리케이션을 테스트 가능하도록 한다.

testImplementation 'org.springframework.boot:spring-boot-starter-test'

 

 - build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.2'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.god'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

▶ 3. Application.java

 - Application.java 소스를 확인해 보면 @SpringBootApplication 이란 어노테이션이 선언되어 있는 것을 확인할 수 있다.  

 

★ 3.1 @SpringBootApplication

 - 해당 어노테이션 하나가 @EnableAutoConfiguration, @ComponentScan, @Configuration을 하나로 묶어 놓은 거라고 볼 수 있다.

 - 스프링부트를 기동하기 위해서는 main 메소드가 필요한데 여기에SpringApplication.run(BoApplication.class, args); 이 부분이 들어 간다.

 - 해당 annotation을 설정한 클래스가 있는 package를 최상위 패키지로 인식하고 ComponentScan을 수행하기 때문에 해당 어노테이션이 있는 클래스의 파일 위치또한 중요하다.
 - 그럼 메인 메서드를 실행 해보자.

C:\work\jdk\corretto-17.0.6\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:6932
Picked up JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
Connected to the target VM, address: '127.0.0.1:54027', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.2)

2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] com.god.bo.BoApplication                 : Starting BoApplication on DESKTOP-9F838L4 with PID 6932 (C:\bo\target\classes started by dedur in C:\bo)
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] com.god.bo.BoApplication                 : No active profile set, falling back to default profiles: default
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.33]
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 4733 ms
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-02-15T18:15:01.949+09:00  INFO 6932 --- [           main] com.god.bo.BoApplication                 : Started BoApplication in 7.114 seconds (JVM running for 8.774)


서버가 기동된 것을 볼 수 있다.

 

▶ 4. index.html

 - 서버는 기동 되었지만, 현재 표현할 페이지가 없다.

 - resources > static 하위에 index.html 파일을 생성 해보자.

 - 그리고 다시 서버를 기동하면 내가 생성한 index.html 파일이 열리는 것을 볼 수 있다. 

   (http://localhost:8080/index.html)

원리는 다음과 같다.

 

★ 4.1 정적 리소스

 - Spring Boot 프로젝트는 별도의 커스터마이징이 없는 경우 정적 리소스의 위치는 다음과 같다.

 

◎ static 
◎ public 
◎ resources 

 META-INF/resources

 

ex) META-INF/resources/test/test.txt 경로에 파일이 위치할 경우 HTTP 상의 요청 주소는 /test/test.txt 이다.

 

이로서 정말 기본적인 스프링 부트 설정이 끝났다.

 

▶ Error Case1

 - Process finished with exit code 0

 

 - 이런 경우 다음 라이브리러가 잘 추가 되어있는지 확인해 보자.

ex) maven

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

ex) gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

 - 안되어 있는 경우, 추가해준 후 Maven 및 Gradle 리로드 하고 확인해 보자.

 

▶ Error Case2

 - No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.0.x was found

 - java version이 맞지 않는 경우이다.

A problem occurred configuring root project 'bo'.
> Could not resolve all files for configuration ':classpath'.
   > Could not resolve org.springframework.boot:spring-boot-gradle-plugin:3.0.2.
     Required by:
         project : > org.springframework.boot:org.springframework.boot.gradle.plugin:3.0.2
      > No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.0.2 was found. The consumer was configured to find a runtime of a library compatible with Java 11, packaged as a jar, and its dependencies declared externally, as well as attribute 'org.gradle.plugin.api-version' with value '7.6' but:
          - Variant 'apiElements' capability org.springframework.boot:spring-boot-gradle-plugin:3.0.2 declares a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares an API of a component compatible with Java 17 and the consumer needed a runtime of a component compatible with Java 11
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')
          - Variant 'javadocElements' capability org.springframework.boot:spring-boot-gradle-plugin:3.0.2 declares a runtime of a component, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java version (required compatibility with Java 11)
                  - Doesn't say anything about its elements (required them packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')
          - Variant 'mavenOptionalApiElements' capability org.springframework.boot:spring-boot-gradle-plugin-maven-optional:3.0.2 declares a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares an API of a component compatible with Java 17 and the consumer needed a runtime of a component compatible with Java 11
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')
          - Variant 'mavenOptionalRuntimeElements' capability org.springframework.boot:spring-boot-gradle-plugin-maven-optional:3.0.2 declares a runtime of a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component compatible with Java 17 and the consumer needed a component compatible with Java 11
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')
          - Variant 'runtimeElements' capability org.springframework.boot:spring-boot-gradle-plugin:3.0.2 declares a runtime of a library, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component compatible with Java 17 and the consumer needed a component compatible with Java 11
              - Other compatible attribute:
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')
          - Variant 'sourcesElements' capability org.springframework.boot:spring-boot-gradle-plugin:3.0.2 declares a runtime of a component, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java version (required compatibility with Java 11)
                  - Doesn't say anything about its elements (required them packaged as a jar)
                  - Doesn't say anything about org.gradle.plugin.api-version (required '7.6')

* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

 

 - Java Version을 17 미만으로 사용하고 싶은 경우는 스프링 부트 3.0 미만으로 다운그레이드 하여 한시적으로 사용 가능할 것이다.

 

※ 본인의 개발 환경이 Java 17로 잘 되어있는지 점검해보자

1) build.gradle

 - 해당 프로젝트의 sourceCompatibility 가 17이상으로 되어있는지 확인.

2) IDEA(Intellij) 설정 확인

 - Gradle JVM (Settings > Build, Execution, Deployment > Build Tools > Gradle )

3) 프로젝트 SDK 설정

 - File > Project Structure..

4) Gradle Refresh

 - 모든 설정 변경 후 Gradle Refresh 하면 정상 적으로 빌드하는것을 볼 수 있다.

 

 

▶ 기록용

- 참고(2020. 4월 설정 시 initializr - 2.2.6)

ex) Maven 으로 설정방법

▶ pom.xml 설정하기

 - Spring Proejct를 직접 설정 해보신분들은 다음과 같이 한번 세팅해보면, Spring Boot의 설정 방법이 얼마나 스프링과 비교하여 간단한지 알 수 있을 것이다.

 

※ 폼설정에 대해 좀더 자세히 포스팅 해두었으니, 아래 포스팅 참고!

https://goddaehee.tistory.com/199

 

★ 2.1 Spring Boot Starter Parent

 - 스프링부트(SpringBoot)에 필요한 dependency를 자동으로 추가하여 준다.

 - spring-boot-starter-parent 아티팩트를 parent 태그에 명시하면 spring-boot-dependencies-x.x.x.RELEASE.pom 파일이 상속된다.

  이에 사용자가 특별히 명시하지 않아도 스프링 부트에서 제공하는 의존성이 자동적으로 설정된다.

 - 프로젝트 설정시 다양한 라이브러리를 사용하게되고, 버전 충돌문제가 늘 발생 한다.

starter-parent는 의존성 조합간의 충돌 문제가 없는 검증 된 버전정보 조합을 제공하여 충돌 문제를 해결하여 준다.

 - 만일 spring-boot-starter-parent 로 상속받은 설정 정보를 다시 재정의하고 싶으면 다음과 같이 properties 태그에 명시하면 된다.  

 

깃헙이나 메이븐 리파지토리 사이트에서 상세 버전정보 조합을 확인 할 수 있다.

 - Maven repository - spring-boot-stater-parent

 - GitHub - spring-boot-stater-parent

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.2.4.RELEASE</version>
</parent>

 

★ 2.1 spring-boot-starter-web

 -  Spring MVC를 사용한 RESTful서비스를 개발하는데 사용.

 

★ 2.2 spring-boot-starter-test

 - Junit, Hamcrest, Mockito를 포함하는 스프링 어플리케이션을 테스트 가능하도록 한다.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

 

일단 프로젝트에 필요한 최소한의 폼 설정으로 진행하고자 다음과 같이 최종적으로 폼 설정을 정리 하였다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion> <!--POM model의 버전-->
	<parent> <!--프로젝트의 계층 정보-->
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.god</groupId> <!--프로젝트를 생성하는 조직의 고유 아이디를 결정한다. 일반적으로 도메인 이름을 거꾸로 적는다.-->
	<artifactId>bo</artifactId> <!--프로젝트 빌드시 파일 대표이름 이다. groupId 내에서 유일해야 한다.Maven을 이용하여 빌드시 다음과 같은 규칙으로 파일이 생성 된다.
		artifactid-version.packaging. 위 예의 경우 빌드할 경우 bo-0.0.1-SNAPSHOT.war 파일이 생성된다.-->
	<version>0.0.1-SNAPSHOT</version> <!--프로젝트의 현재 버전, 프로젝트 개발 중일 때는 SNAPSHOT을 접미사로 사용-->
	<packaging>war</packaging> <!--패키징 유형(jar, war, ear 등)-->
	<name>bo</name> <!--프로젝트, 프로젝트 이름-->
	<description>Demo project for Spring Boot</description> <!--프로젝트에 대한 간략한 설명-->
	<url>http://goddaehee.tistory.com</url> <!--프로젝트에 대한 참고 Reference 사이트-->

	<properties>
	<!-- 버전관리시 용이 하다. ex) 하당 자바 버전을 선언 하고 dependencies에서 다음과 같이 활용 가능 하다.
	<version>${java.version}</version> -->
		<java.version>1.8</java.version>
	</properties>

	<dependencies> <!--dependencies태그 안에는 프로젝트와 의존 관계에 있는 라이브러리들을 관리 한다.-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build> <!--빌드에 사용할 플러그인 목록-->
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

On linux

 

jboss가 설치된 폴더의 bin폴더로 갑니다.

./domain.sh를 쳐줍니다~

$ cd jboss_home/bin $ ./domain.sh

 

On Windows:

 

> cd jobss_home/bin > domain.bat

 

This will launch a total of 5 processes on your system: three JBoss AS server instances; a Domain Controller process that acts as a central management point for all servers that belong to the same "domain"; and a lightweight Process Controller process that is responsible for spawning the other 4 processes and monitoring their lifecycle.

 

If you want to work in standalone mode, open a terminal and cd into the distribution's bin directory, and run the "standalone" launch script:

 

standalone mode 일 경우.

 

On linux

 

$ cd jboss_home/bin

$ ./standalone.sh

 

On Windows:

 

 

> cd jboss_home/bin

> standalone.bat

 

 

 

 

종료하는 방법

 

 


$ cd jboss_home/bin $ ./jboss-cli.sh --connect command=:shutdown

 

The "--connect" by default connects to localhost at port 9999 and triggers the shutdown. If your server doesn't use the default port or isn't bound to localhost, then you can explicitly specify the host port combination to the --connect as follows:

 

$ ./jboss-cli.sh --connect controller=<IP>:<port> command=:shutdown

public String main(HttpServletRequest req,Model model,Authentication authentication
,Gson gson,InfoVo infoVo
) throws Exception{


String UploadPath1,UploadPath2;


String[] mapperArr = {"noti", "banner"};
String[] serviceArr = {"notiData", "bannerData"};
infoVo.setcode("list"); 
infoVo.setindexPage(0);
infoVo.setpageSize(10000);
String str = "banner"; 
List<InfoVo> Lst = infoService.bannerData(infoVo);  
UploadPath1 = gp.getProperty("upload.server.uploadPath")+"/";
UploadPath2 = gp.getProperty("upload.server.uploadPath")+"/"+str+"/";
File Folder2 =new File(UploadPath2); // 생성돈 신규 폴더
File Folder1 =new File(UploadPath1);  // 기존 업로드 폴더

if (!Folder2.exists()) {
try{
Folder2.mkdir(); //폴더 생성합니다.
    System.out.println("폴더가 생성되었습니다.");       
    log.debug(Lst.size());
        } 
        catch(Exception e){
    e.getStackTrace();
}        
         }else {
System.out.println("이미 폴더가 생성되어 있습니다.");
}

log.debug(Lst.size());
     for(int i=0;i<Lst.size();i++) {
     File cpFile = new File(Folder1+"/"+Lst.get(i).getATCH1().toString());
     File cpdFile = new File(Folder2+"/"+Lst.get(i).getATCH1().toString()); 
        FileInputStream input = new FileInputStream(cpFile);
        FileOutputStream output = new FileOutputStream(cpdFile);
        byte[] buf = new byte[4096];
        int readData;
        while ((readData = input.read(buf)) > 0) {
            output.write(buf, 0, readData);
        }
        input.close();
        output.close();    
     }
     

     
return "/config/main";
}

String resultImg = setImageSize(width1,height1,setwidth,setheight);

log.debug("resultImg : "+resultImg);

res.setContentType("text/html; charset=UTF-8");
PrintWriter out = res.getWriter(); 
if(resultImg.equals("good")) {
code=0;
}else if (resultImg.equals("badwidth")) {
  out.println("<script language='javascript'>");
out.println("alert('이미지 가로 비율이  맞지 않습니다. px사이즈 확인후 다시 업로드해 주십시요')"); 
out.println("</script>"); 
out.flush(); 
code=1;
}else if (resultImg.equals("badheight")) { 
out.println("<script language='javascript'>");
out.println("alert('이미지세로 비율이  맞지 않습니다. px사이즈 확인후 다시 업로드해 주십시요')"); 
out.println("</script>"); 
out.flush();
code=2;
}
  

//설정된 기준값과 어로드 받은 파일의 가로세로 값의 비율이 같은지 비교
public String setImageSize(double width1,double height1,double setwidth,double setheight){ 
                String Result="good";
                
                double result=0;
                if(width1>height1) {
                               result =  (setheight * width1) /height1;
                               log.debug("width1 > height1 = result : "+result);
                               if(result < (setwidth - 30) || result > (setwidth+30)) { 
                                Result ="badwidth";
                               } 
                            }else {
                               result =  (setwidth * height1) /width1;
                               if(result < (setheight - 30) || result > (setheight+30)) { 
                                Result ="badheight";
                               } 
                         }  
             return Result;
             }

String resultImg = setImageSize(width1,height1,setwidth,setheight);
		
		log.debug("resultImg : "+resultImg);
		
		res.setContentType("text/html; charset=UTF-8");
	PrintWriter out = res.getWriter(); 
		if(resultImg.equals("good")) {
			code=0;
		}else if (resultImg.equals("badwidth")) {
			  out.println("<script language='javascript'>");
	out.println("alert('이미지 가로 비율이  맞지 않습니다. px사이즈 확인후 다시 업로드해 주십시요')"); 
	out.println("</script>"); 
	out.flush(); 
	code=1;
		}else if (resultImg.equals("badheight")) { 
	out.println("<script language='javascript'>");
	out.println("alert('이미지세로 비율이  맞지 않습니다. px사이즈 확인후 다시 업로드해 주십시요')"); 
	out.println("</script>"); 
	out.flush();
	code=2;
		}
		  
	
	//설정된 기준값과 어로드 받은 파일의 가로세로 값의 비율이 같은지 비교
	public String setImageSize(double width1,double height1,double setwidth,double setheight){ 
            			   String Result="good";
            			   
            			   double result=0;
            			   if(width1>height1) {
                               result =  (setheight * width1) /height1;
                               log.debug("width1 > height1 = result : "+result);
	                               if(result < (setwidth - 30) || result > (setwidth+30)) { 
	                                Result ="badwidth";
	                               } 
                            }else {
                               result =  (setwidth * height1) /width1;
                               if(result < (setheight - 30) || result > (setheight+30)) { 
                            	   Result ="badheight";
                               } 
                         }  
            			return Result;
            		}

'study > java' 카테고리의 다른 글

JBOSS start stop. whildfly  (0) 2023.02.23
파일복사하기  (0) 2023.02.17
설정된 비율로 이미지 업로드 하기 #java  (0) 2023.02.13
Java 11 HttpClient (자바11 HttpClient) 기능 살펴 보기  (0) 2023.01.29
restAPI짭퉁  (0) 2022.12.30

  /*
     *  @author 박원쥬스
     *  @Description banner  inser update
      * @param fileName  file
     *  @return fileurl 
     */
      @RequestMapping(value="/config/bannerSQL") 
      public String bannerSQL(@RequestParam Map<String, Object> requestMap,ModelMap model
          ,InfoVo infoVo ,HttpSession session         
          , HttpServletResponse res,HttpServletRequest req) throws Exception {
      String userid = ""+session.getAttribute("userid"); 
      InetAddress ipAddress = InetAddress.getLocalHost();
        String ip = ipAddress.getHostAddress().toString();        
        UploadPath = gp.getProperty("upload.server.uploadPath"); 
      String directory = UploadPath;   
        int sizeLimit = 100*1024*1024;   
      MultipartRequest multi = new MultipartRequest(req, directory, sizeLimit, "UTF-8", new MyFileRenamePolicy() );
          
          String idx = multi.getParameter("idx"); 
          infoVo.setidx(multi.getParameter("idx"));
          infoVo.setATCH1(multi.getFilesystemName("ATCH1"));
          infoVo.setLINK1(multi.getParameter("LINK1"));
          infoVo.setPOSITION(multi.getParameter("POSITION")); 
          infoVo.setALT(multi.getParameter("ALT")); 
          infoVo.setDEL_YN(multi.getParameter("DEL_YN"));   
          infoVo.settemp1(multi.getParameter("temp1"));    
          
          int code = 0;
          if(multi.getFilesystemName("ATCH1")!=null) {
                     String fileName=multi.getFilesystemName("ATCH1");
                        BufferedImage originalImage = ImageIO.read(new File(UploadPath + fileName));  
                        double width1 = originalImage.getWidth();
                        double height1 =originalImage.getHeight();  
                        
                        String POSITION = multi.getParameter("POSITION");
                        
                        double setwidth=0;
                        double setheight=0;
                        
                        if(POSITION.equals("main") || POSITION.equals("bwaveMain")) {
                          setwidth=1280;
                          setheight=550;
                        }
if(POSITION.equals("footer")) {
 setwidth=200;
                          setheight=60;
}  

                       
log.debug("업로드 가로{}"+width1);
                    log.debug("업로드 세로{}"+height1);
                    log.debug("업로드 setwidth{}"+setwidth);
                    log.debug("업로드 setheight{}"+setheight);
                       
                        //(1200 / 1600) x 400 = 300
                        double result=0.0;
                        
                     
                         if(width1>height1) {
                         result =  (setheight * width1) /height1;
                         log.debug("width1 > height1 = result : "+result);
                         if(result < (setwidth - 30) || result > (setwidth+30)) {
                           res.setContentType("text/html; charset=UTF-8");
                          PrintWriter out = res.getWriter(); 
                          out.println("<script language='javascript'>");
                          out.println("alert('이미지가로비율이 맞지 않습니다."+result+"px사이즈  확인후 다시 업로드해 주십시요')"); 
                        
                          out.println("</script>"); 
                          out.flush();
                          code=1;
                         }
                          
                         }else {
                         result =  (setwidth * height1) /width1;
                         if(result < (setheight - 30) || result > (setheight+30)) {
                           res.setContentType("text/html; charset=UTF-8");
                          PrintWriter out = res.getWriter(); 
                          out.println("<script language='javascript'>");
                          out.println("alert('이미지세로 비율이  맞지 않습니다."+result+"px사이즈 확인후 다시 업로드해 주십시요')"); 
                           
                          out.println("</script>"); 
                          out.flush(); 
                          code=2;
                         } 
                         } 
                    }
          
          
          
          
          if ( idx.equals("") && idx.length()==0) {
           // 수정
            infoService.bannerInsert(infoVo);
          }else {
            //등록 
            infoService.bannerUpdate(infoVo);
          } 
          if(code==0) {
           log.debug("good");
          }
          
          
      return "redirect:/config/banner"; 
    }

'study > java' 카테고리의 다른 글

파일복사하기  (0) 2023.02.17
업로드 이미지 설정값의 비율과 같으면 업로드 되게  (0) 2023.02.13
Java 11 HttpClient (자바11 HttpClient) 기능 살펴 보기  (0) 2023.01.29
restAPI짭퉁  (0) 2022.12.30
JSTL  (0) 2022.12.30

Java 11 HttpClient (자바11 HttpClient) 기능 살펴 보기

 

 방문해 주셔서 감사합니다! 항상 행복하세요!

   - 이것저것 다 하기를 좋아하는 9년차 웹개발자 "마샤와곰" 입니다 * C빼고..
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com



Java(자바)/Java 기본

Java 11 HttpClient (자바11 HttpClient) 기능 살펴 보기

야근없는 행복한 삶을 위해 ~ 
by 마샤와 곰 

자바가 11버전으로 업그레이드 되면서 java.net 패키지에 http관련된 라이브러리가 추가 되었습니다.

Java11 이전에는 대부분 아파치에서 제공하는 라이브러리를 사용 했었습니다.

 * Apache Http Client(org.apache.httpcomponents)

 

먼저 살펴 볼 방법은 get 방식으로 요청하는 방법 입니다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HttpClientJava11 {

    public static void get(String address, String[] headers) throws Exception {
        //헤더값은 이런식으로 사용합니다(키, 값)headers => "Content-type","plain/text"  
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        String result = client.sendAsync(
            HttpRequest.newBuilder(
                new URI(address)).GET().headers(headers).build(),  //GET방식 요청
                HttpResponse.BodyHandlers.ofString()  //응답은 문자형태
            ).thenApply(HttpResponse::body)  //thenApply메소드로 응답body값만 받기
            .get();  //get메소드로 body값의 문자를 확인
        System.out.println(result);
    }
}    

 

HttpClient 클래스에 존재하는 newBuilder라는 static 메소드를 호출하여 클라언트 객체를 만들어 줍니다.

만들어진 클라이언트 객체에서 sendAsync라는 메소드를 사용하면 이름처럼 "비동기화" 된 요청을 할 수 있습니다.

동기적 요청을 하려면 send 메소드를 호출하여 주면 됩니다.

 

sendAsync 메소드에서 필요한 파라미터는 2개 입니다.

 1) HttpRequest request : 요청하는 형태, 데이터를 정의 합니다.

 2) BodyHandler<T> responseBodyHandler : 응답 받을 형태를 관리 합니다.

 

위 예제는 URI 클래스에서 get방식으로 요청을 하는 모습이며, 응답받는 데이터는 문자형태( BodyHandlers.ofString() )로 받도록 정의 된 모습입니다.

sendAsync메소드는 CompletableFuture라는 클래스를 제공합니다.

해당 클래스는 시간이 걸릴 수 있는 작업을 Future 내부로 작성하고 호출자 스레드가 결과를 기다리는 동안 다른 유용한 작업을 할 수 있는 기능을 제공합니다.   * 출처 : 모던 자바인 액션

 

thenApply를 통해서 Responsebody 값만 가져오게 한 뒤에 마지막에 get 메소드를 호출하면 응답결과 메시지만 받을 수 있습니다.

몇번 사용하다보면 익숙해 질 수 있습니다.

 

다음으로 살펴 볼 방법은 post 방법 입니다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HttpClientJava11 {

    public static void post(String address, Map<?, ?> params, String[] headers) throws Exception {

        ObjectMapper objectMapper = new ObjectMapper();
        String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(params);

        BodyPublisher body = BodyPublishers.ofString(requestBody);  //post 파라미터 최종 모습 입니다.

        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();

        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).POST(body).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
}    

 

post 메소드는 전송 할 데이터가 일반적으로 key, value 형태로 되어 있기 때문에 map 데이터를 변환해 주는 ObjectMapper 클래스를 사용하였습니다.

메소드를 사용하는 방법은 get 메소드와 매우 비슷합니다.

또한 body로 구현된 BodyPublisher 클래스에서 subscribe를 통해서 해당 행위에 대한 구독을 할 수 있습니다.

requestBody의 발행 조건에 대해 구독자를 추가 할 수 있습니다.

* 사실 사용법에 대해서는 연습(?) 중 입니다..ㅠ_ㅠ

 

또 다른 요청 형태인 put이나 delete방식 사용법은 거의 동일합니다.

put은 post처럼 요청하면 되며, delete는 get처럼 요청하면 됩니다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HttpClientJava11 {

    //get 요청 방법
    public static void get(String address, String[] headers) throws Exception {
        //헤더값은 이런식으로 사용합니다(키, 값)headers => "Content-type","plain/text"  
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        String result = client.sendAsync(
            HttpRequest.newBuilder(
                new URI(address)).GET().headers(headers).build(),  //GET방식 요청
                HttpResponse.BodyHandlers.ofString()  //응답은 문자형태
            ).thenApply(HttpResponse::body)  //thenApply메소드로 응답body값만 받기
            .get();  //get메소드로 body값의 문자를 확인
        System.out.println(result);
    }

    //post 요청 방법
    public static void post(String address, Map<?, ?> params, String[] headers) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(params);
        BodyPublisher body = BodyPublishers.ofString(requestBody);  //post 파라미터 최종 모습 입니다.
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).POST(body).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
    
    //delete 요청 방법
    public static void delete(String address, String[] headers) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        String result = client.sendAsync(
            HttpRequest.newBuilder(
                new URI(address)).DELETE().headers(headers).build(),  //DELETE방식 요청
                HttpResponse.BodyHandlers.ofString()
            ).thenApply(HttpResponse::body)
            .get();
        System.out.println(result);
    }
    
    //put 요청 방법
    public static void put(String address, Map<?, ?> params, String[] headers) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(params);
        BodyPublisher body = BodyPublishers.ofString(requestBody);  
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).PUT(body).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }    
}    

 

마지막으로 살펴볼 기능은 파일 전송을 위한 방법 입니다.

파일 전송은 조금 어려운 점이 데이터를 byte단위로 쪼개어 전송을 해야 된다는 점 입니다.

BodyPublishers에서 제공하는 ofFile을 이용해서 파일을 전송해보려 했는데 문제가 발생했습니다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HttpClientJava11 {


    //multipart 요청 방법
    public static void multipart(String address, String[] headers) throws Exception {
        BodyPublisher fileBody = BodyPublishers.ofFile(Paths.get("파일경로/test.csv"));
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).POST(fileBody).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
        
}    

 

이렇게 작성하다 보니까...파일은 보내질 것 같은데 파라미터는 어떻게 보내는지 감이오지 않았습니다.

단순하게 파일전송만 하는 경우는 많지 않으니 사실 사용할 수 없는 방법이였습니다.

그래서 다음으로 검색하여 적용한 부분이 데이터를 바이트 단위로 변환하는 방법이였습니다.

BodyPublisher에 들어가는 데이터를 바이트단위의 배열로 채우는 방법 입니다.

    public static void multipart(String address, Map<?, ?> params) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        HttpRequest request = HttpRequest.newBuilder().uri(new URI(address))
            .headers("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(oMultipartData(params, boundary))
            .build();  //BodyPublisher를 바이트단위의 형태로 넣습니다.

        String result = client
            .sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
	
	
    private static BodyPublisher oMultipartData(Map<?, ?> data, String boundary) throws IOException {
        var byteArrays = new ArrayList<byte[]>();
        byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=")
            .getBytes(StandardCharsets.UTF_8);
        for (Map.Entry<?, ?> entry : data.entrySet()) {
            byteArrays.add(separator);
            if (entry.getValue() instanceof Path) {  //파일이면
                var path = (Path) entry.getValue();
                String mimeType = Files.probeContentType(path);
                byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\r\nContent-Type: "
                    + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
                byteArrays.add(Files.readAllBytes(path));
                byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
            } else {  //단순 데이터면
                byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n")
                    .getBytes(StandardCharsets.UTF_8));
            }
        }
        byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8));
        return BodyPublishers.ofByteArrays(byteArrays);
    }

 

multipart메소드에서 Map객체를 받도록 하였습니다.

Map객체 샘플과 요청 방법 모습은 아래와 같습니다.

{
    HashMap<Object, Object> param = new HashMap<>();
    param.put("req", 1234);
    param.put("req1", "fefe");
    param.put("req2", new String[] {"abcd","1234"});
    param.put("file_", Paths.get("E:/tester.jpg"));  //파일 데이터1
    param.put("file_", Paths.get("E:/re_filename.txt")); //파일 데이터2
    
    multipart("http://127.0.0.1:8181/testValid2", param);  // 전송
}

 

위 내용을 전부 정리하여 본 클래스 입니다.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HttpClientJava11 {

    private static final String boundary = "-------------oiawn4tp89n4e9p5";
    
    public void tester() throws Exception {
        //get방식
        get("http://주소?param=1234", new String[] {"Content-type","applacation/json"});
        
        HashMap<Object, Object> param = new HashMap<>();
        param.put("req1", 1234);
        param.put("req2", "abcd");
        
        //post방식
        post("http://주소", param , new String[] {"Content-type","applacation/json"});
    
        //get방식
        delete("http://주소?param=1234", new String[] {"Content-type","applacation/json"});
        
        //put방식
        put("http://주소", param , new String[] {"Content-type","applacation/json"});  

        //파일정보 추가
        param.put("file1", Paths.get("파일경로/파일명"));
        param.put("file2", Paths.get("파일경로/파일명"));

        //파일전송
        multipart("http://주소", param)
    }

    //get 요청 방법
    public static void get(String address, String[] headers) throws Exception {
        //헤더값은 이런식으로 사용합니다(키, 값)headers => "Content-type","plain/text"  
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        String result = client.sendAsync(
            HttpRequest.newBuilder(
                new URI(address)).GET().headers(headers).build(),  //GET방식 요청
                HttpResponse.BodyHandlers.ofString()  //응답은 문자형태
            ).thenApply(HttpResponse::body)  //thenApply메소드로 응답body값만 받기
            .get();  //get메소드로 body값의 문자를 확인
        System.out.println(result);
    }

    //post 요청 방법
    public static void post(String address, Map<?, ?> params, String[] headers) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(params);
        BodyPublisher body = BodyPublishers.ofString(requestBody);  //post 파라미터 최종 모습 입니다.
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).POST(body).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
    
    //delete 요청 방법
    public static void delete(String address, String[] headers) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        String result = client.sendAsync(
            HttpRequest.newBuilder(
                new URI(address)).DELETE().headers(headers).build(),  //DELETE방식 요청
                HttpResponse.BodyHandlers.ofString()
            ).thenApply(HttpResponse::body)
            .get();
        System.out.println(result);
    }
    
    //put 요청 방법
    public static void put(String address, Map<?, ?> params, String[] headers) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        String requestBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(params);
        BodyPublisher body = BodyPublishers.ofString(requestBody);  
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        URI url = new URI(address);
        String result = client.sendAsync(HttpRequest.newBuilder(url).PUT(body).headers(headers).build(),
            HttpResponse.BodyHandlers.ofString()).thenApply(HttpResponse::body).get();
        System.out.println(result);
    }    
    
    //파일 전송 방법
    public static void multipart(String address, Map<?, ?> params) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        HttpRequest request = HttpRequest.newBuilder().uri(new URI(address))
            .headers("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(oMultipartData(params, boundary))
            .build();  //BodyPublisher를 바이트단위의 형태로 넣습니다.

        String result = client
            .sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body).get();
        System.out.println(result);
    }
	
    //multipart메소드에서 사용하는 메소드
    private static BodyPublisher oMultipartData(Map<?, ?> data, String boundary) throws IOException {
        var byteArrays = new ArrayList<byte[]>();
        byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=")
            .getBytes(StandardCharsets.UTF_8);
        for (Map.Entry<?, ?> entry : data.entrySet()) {
            byteArrays.add(separator);
            if (entry.getValue() instanceof Path) {  //파일이면
                var path = (Path) entry.getValue();
                String mimeType = Files.probeContentType(path);
                byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName() + "\"\r\nContent-Type: "
                    + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
                byteArrays.add(Files.readAllBytes(path));
                byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
            } else {  //단순 데이터면
                byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n")
                    .getBytes(StandardCharsets.UTF_8));
            }
        }
        byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8));
        return BodyPublishers.ofByteArrays(byteArrays);
    }    
}    

 

이상으로 자바11에서 HttpClient를 활용하여 데이터를 주고받는 방법에 대해서 살펴 보았습니다.

아직 자세히 살펴본 내용이 아니라 다소 빈약한 부분이 존재합니다.  * 그래도 테스트는 전부 성공 했습니다!!

좋은 피드백과 의견 있으면 언제든 알려주세요! :)

 

 

+ Recent posts