<< 학습 목표 >>

1. 회원 탈퇴 서비스를 구현할 수 없는 이유를 설명할 수 있다.


전 글 ( https://codingaja.tistory.com/44, https://codingaja.tistory.com/47, https://codingaja.tistory.com/48 ) 에서 프로젝트에 회원가입, 로그인, 회원정보수정 서비스를 구현했음

 

비록 회원 정보 수정 서비스를 완전히 다 구현하진 못했지만...

CRUD 서비스 중 CRU 를 구현했으므로 이제 D(Delete) 를 구현하자

회원 탈퇴가 대표적인 D 서비스임

 

회원 탈퇴 서비스는 회원 정보를 삭제(Delete) 하는 서비스임

 

우선 기능에 대한 정의, 스토리보드, 인터페이스를 작성하자

<< 기능에 대한 정의 >>
로그인을 한 사용자만 이용할 수 있는 기능으로 회원 탈퇴 버튼을 눌렀을 때 로그인을 한 사용자의 정보를 삭제함
<< 스토리 보드 >>
<< 인터페이스 >>

회원 탈퇴 버튼이 있는 페이지는 우리가 앞서 로그인 서비스를 구현할 때 추가했었음

( https://codingaja.tistory.com/47 )

 

회원 탈퇴 버튼이 있는 페이지는 webapp -> member -> index.html 임

회원 탈퇴 버튼이 구현되어있으니 별도의 코드를 입력할 필요는 없음

 

그. 러. 나. 회원 탈퇴 서비스는 회원 정보 수정 서비스와 마찬가지로 아직은 구현할 수 없음

회원 탈퇴를 하기 위해서는 로그인을 한 사용자가 누구인지 알아야하고 그러기 위해서는 쿠키(Cookie) 나 세션(Session) 에서 로그인을 한 사용자의 ID 를 저장해둬야한다고 했음

쿠키와 세션은 다음 장인 Chapter05 에서 배울 것임 

 

지금은 회원 탈퇴 서비스를 왜 구현할 수 없는지까지만 기억하자

회원 탈퇴 서비스는 다음 장에서 구현할 것

 

여기까지 회원 탈퇴 서비스 끝~!


여기까지 글의 개수만 놓고 보면 제일 많은 Chapter04 가 끝났음

지금 우리는 서블릿의 2 / 3 를 배운 것이므로 지금까지 배운 것들을 다시 한번, 그리고 또 한번 복습하자

 

간혹 복습을 전혀 하지 않고 진도만 따라오거나 복습을 딱 한번만 하고 진도를 따라오는 분들이 있는데 그렇게 복습을 적게 하면 절대 서블릿을 내껄로 만들 수 없음

 

운동 선수들을 보면 프로 운동 선수가 되기 위해 기초 체력을 매일 기름 또한 기본적인 자세, 응용 자세 등 다양한 것들을 매일 매일 연습함

우리도 개발 준비생이 아닌 개발 선수가 되기 위한 과정이므로 배웠던 것을 또 보고 또 보고 여러번 반복해야함

그러나 우리는 운동 선수와 비교했을 때 앞날이 굉장히 창창함

운동 선수들은 그렇게 열심히 운동해도 순위권에 들지 못하면 대회에 나갈 기회 조차 잃어버림

그리고 그 대회의 종류나 횟수가 매우 제한적임

개발 선수가 되고 싶은 우리는? 개발자 취업 준비생의 순위 라는게 없을 뿐더러 내가 지원 할 수 있는 회사는 널리고 널렸음

 

반드시 복습을 해야하고 한번이 아닌 여러번 해야함

728x90
LIST

<< 학습 목표 >>

1. 정적인 컨텐츠를 구현하기 위한 프로그래밍 언어를 나열할 수 있다.

2. 동적인 컨텐츠를 구현하기 위한 프로그래밍 언어를 나열할 수 있다.

3. 동적인 컨텐츠를 구현하기 위한 두 가지 방법을 설명할 수 있다.

4. 회원 정보 수정 서비스를 구현할 수 없는 이유를 설명할 수 있다.


전 글 ( https://codingaja.tistory.com/44, https://codingaja.tistory.com/47 ) 에서 프로젝트에 회원가입, 로그인 서비스를 구현했음

CRUD 중 CR 을 구현했으므로 이제 U(Update) 를 구현하자

회원 정보 수정이 대표적인 U 서비스임

 

회원 정보 수정 서비스는 회원 정보를 수정(Update) 하는 서비스임

 

우선 기능에 대한 정의, 스토리보드, 인터페이스를 작성하자

<< 기능에 대한 정의 >>
로그인을 한 사용자만 이용할 수 있는 기능으로 회원 정보 수정 페이지에 들어왔을 때 로그인을 한 사용자의 정보를 보여주고 아이디를 제외한 비밀번호, 닉네임, 연락처를 새로운 데이터로 바꿀 수 있음

비밀번호를 변경하고 싶다면 비밀번호와 새 비밀번호를 입력함
비밀번호와 비밀번호 확인란은 비밀번호를 변경하는 경우에만 입력함

비밀번호와 비밀번호 확인은 서로 일치해야함
닉네임과 연락처는 회원가입의 조건과 동일함
<< 스토리 보드 >>
<< 인터페이스 >>

이제 회원 정보 수정 서비스를 구현하자

먼저 웹 페이지부터

webapp -> member -> update.html 을 추가하고 아래 코드를 추가하자

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/join.css">
</head>
<body>
	<header>&#60;&#60; 회원 정보 수정 &#62;&#62;</header>
	<main>
		<form action="/studyProject/member/update" method="POST">
			<fieldset>
				<label for="id">아이디</label>
				<input type="text" name="id" id="id" required="required" disabled="disabled" readonly="readonly" dp-name="아이디">
			</fieldset>
			
			<fieldset>
				<div>
					<label for="pw">비밀번호</label>
					<input type="password" name="pw" id="pw" required="required" dp-name="비밀번호">
				</div>
				<div>
					<label for="pwchk">비밀번호 확인</label>
					<input type="password" name="pw" id="pwchk" required="required" dp-name="비밀번호 확인">
				</div>
			</fieldset>
			
			<fieldset>
				<label for="nickname">닉네임</label>
				<input type="text" name="nickname" id="nickname" required="required" dp-name="닉네임">
			</fieldset>
			
			<fieldset>
				<label for="tel">연락처</label>
				<input type="tel" name="tel" id="tel" required="required" dp-name="연락처">
			</fieldset>
			<fieldset>
				<button type="button" role="submit">정보 수정</button>
			</fieldset>
		</form>
	</main>
</body>
</html>

 

! 여기서 ! 굉장히 큰 문제가 하나 생김

그 큰 문제에 대해 얘기하기 전에 정적인 컨텐츠와 HTML에 대해 얘기하고 여기서 발생한 큰 문제에 대해서 얘기하자

 

HTML은 정적인 컨텐츠를 보여주는 언어로 회원가입, 로그인 페이지는 누구나 다 똑같은 페이지(정적인 컨텐츠)를 봐야함

A 사용자는 회원 가입할 때 아이디 입력란이 안보였는데 B 사용자는 회원 가입할 때 아이디 입력란이 보이는 등 [ 사용자의 상황에 따라 다른 컨텐츠 (동적인 컨텐츠) ] 가 보이면 안되는 페이지들임

그래서 앞서 회원가입, 로그인 페이지는 정적인 컨텐츠이므로 HTML로 작성해도 문제가 없었음

 

이제 이 페이지에서 발생하는 굉 장 히 큰 문제를 얘기하자

 

위로 올라가서 회원 정보 수정 기능에 대한 정의를 보면 "로그인을 한 사용자의 정보를 보여주고" 라는 말이 있음

로그인을 한 사용자의 정보를 보여준다는 말은 A가 로그인 후 회원 정보 수정 페이지로 왔다면 A의 정보가 보여야함

B가 로그인 후 회원 정보 수정 페이지로 왔다면 B의 정보가 보여야함

 

다르게 생각해보면 내가 네이버에 로그인하고 네이버에서 사용하는 닉네임을 바꾸기 위해서 네이버의 회원 정보 수정 페이지로 들어갔다고 생각해보자

그러면 내 정보가 보여야함

다른 회원의 정보가 보이면? 당연히 이상함

 

이런것처럼 우리의 회원 정보 수정 페이지에서도 로그인을 한 사용자의 정보를 보여줘야하는데 HTML은 정적인 컨텐츠를 보여주는 언어임

HTML만 사용해서는 회원 정보 수정 페이지에서 로그인한 사용자의 정보를 보여줄 수 없음

이제 여기에 동적인 컨텐츠를 보여줄 수 있는 언어를 사용해 로그인한 사용자의 정보를 보여주면 됨

동적인 컨텐츠를 보여줄 수 있는 언어는? 클라이언트 사이드 언어인 JS 가 있고 서버 사이드 언어인 Servlet 이 있음

 

둘 중에 뭘 사용해도 상관 없음

JS 를 사용한다면 XMLHttpRequest 객체를 사용하거나 Jquery의 Ajax 를 사용해 로그인 한 사용자의 정보를 불러와 화면에 뿌려줄 수 있을 것임

Servlet을 사용한다면 회원 정보 수정 페이지를 보여주기 전 로그인 한 사용자의 정보를 불러와 Servlet 안에서 PrintWriter 객체를 사용해 로그인한 사용자의 정보가 들어간 회원 정보 수정 페이지를 print 하면 됨

 

그. 러. 나. 여전히 큰 문제는 해결되지 않았음

가장 중요한 로그인한 사용자를 판단할 수단이 없음

로그인을 한 사용자가 누구인지 알려면 로그인을 할 때 쿠키(Cookie) 또는 세션(Session) 이라는 곳에 로그인한 사용자의 ID 를 저장해둬야함

앞 글 ( https://codingaja.tistory.com/47 ) 에서 로그인할 때 우리가 쿠키나 세션을 사용했는지? 사용하지 않았음

쿠키와 세션은 다음 장인 Chapter05 에서 배움

 

따라서 큰 문제를 해결 할 수 있는 방법은 아직 모름

지금은 회원 정보 수정 페이지를 추가하고 회원 정보 수정 서비스를 왜 구현할 수 없는지까지만 기억하자

회원 정보 수정 서비스는 다음 장에서 구현할 것

 

여기까지 회원 정보 수정 서비스 끝~!

728x90
LIST

<< 학습 목표 >>

1. 로그인 서비스를 구현할 수 있다.


전 글 ( https://codingaja.tistory.com/44 ) 에서 프로젝트에 회원 가입 서비스를 구현했음

CRUD 중 C(Create) 를 구현했으므로 이제 R(Read) 을 구현하자

로그인이 대표적인 R 서비스임

 

사용자가 아이디, 비밀번호를 입력하고 [ 로그인 ] 버튼을 누르면 서버는 사용자가 입력한 아이디, 비밀번호를 받아 DB에서 회원 정보를 조회(Read) 함

 

우선 기능에 대한 정의, 스토리보드, 인터페이스를 작성하자

<< 기능에 대한 정의 >>
로그인 하지 않은 사용자가 사용할 수 있는 기능으로 아이디, 비밀번호를 입력한 후 로그인을 함

아이디와 비밀번호를 정확하게 입력했다면 로그인 성공 페이지로 이동
아이디 또는 비밀번호를 부정확하게 입력했다면 로그인 실패 알림이 뜸
<< 스토리 보드 >>
1. 로그인 시



2. 로그인 실패 시 

<< 인터페이스 >>

먼저 화면부터 만들자

webapp -> member -> login.html 을 추가하고 아래 코드를 추가하자

<< 로그인 HTML >>

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/login.css">
</head>
<body>
		<header>&#60;&#60; 로그인 &#62;&#62;</header>
		<main>
			<form action="/studyProject/member/login" method="POST">
				<fieldset>
					<label for="id">아이디</label>
					<input type="text" name="id" id="id" required="required" dp-name="아이디">
				</fieldset>
				<fieldset>
					<div>
						<label for="pw">비밀번호</label>
						<input type="password" name="pw" id="pw" required="required" dp-name="비밀번호">
					</div>
				</fieldset>
				<fieldset>
					<button type="button" role="submit">로그인</button>
				</fieldset>
			</form>
		</main>
</body>
</html>

 

<< 로그인 CSS >>

webapp -> css -> member -> login.css 를 추가하고 아래 코드를 추가하자

더보기

@charset "UTF-8";

fieldset {
	margin-bottom: 10px;
}

body > main > form > fieldset:nth-child(3) {
	display: flex;
	justify-content: end;
}

label {
	display: inline-block;
	width: 102px;
	text-align: right;
}

 

<< 메인 HTML >>

webapp -> member -> index.html 을 추가하고 아래 코드를 추가하자

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/index.css">
</head>
<body>
	<header>&#60;&#60; 메인 &#62;&#62;</header>
	<main>
		<ul>
			<li><a href="/studyProject/member/join.html">회원가입</a></li>
			<li><a href="/studyProject/member/login.html">로그인</a></li>
			<li><a href="#">회원 정보 수정</a></li>
			<li><a href="#">회원 탈퇴</a></li>
		</ul>
	</main>
</body>
</html>

 

<< 메인 CSS >>

webapp -> css -> index.css 를 추가하고 아래 코드를 추가하자

더보기

@charset "UTF-8";

li {
	margin-bottom: 5px;
}

 

추가한 페이지들을 확인해보자


이제 인터페이스를 보며 서블릿을 만들자

 

서블릿에서는 요청을 받아 파라미터를 꺼낸 후 검증을 함

검증에 통과했다면 서비스를 호출해 아이디, 비밀번호를 정확하게 입력했는지 확인할 것

 

서비스는 DAO 를 생성한 다음 회원 가입 때 선언해뒀던 selectById 메서드를 호출해 사용자가 입력한 아이디로 회원 정보를 조회할 것

조회된 회원 정보가 있다면 비밀번호 일치 여부를 확인함

비밀번호까지 일치한다면 로그인 성공

사용자가 입력한 아이디로 조회된 회원 정보가 없거나 조회된 회원 정보는 있지만 회원 정보의 비밀번호와 사용자가 입력한 비밀번호가 다르다면 로그인 실패

 

<< MemberService 클래스 >>

더보기

package member;

public class MemberService {
	public boolean correctIdNPw(MemberDto member) {
		MemberDao dao = new MemberDao();
		
		MemberDto selectedMember = dao.selectOneById(member.getId());
		if(selectedMember == null) {
			return false;
		}
		
		// 사용자가 입력한 비밀번호
		String userInputPw = member.getPw();
		// 사용자가 입력한 아이디로 조회한 회원 정보의 비밀번호
		String storedUserPw = selectedMember.getPw();
		
		if(!userInputPw.equals(storedUserPw)) {
			return false;
		} else {
			return true;
		}
	}
    
    ...
}

 

chapter04 -> MemberLogin 서블릿을 추가하고 아래 코드를 입력하자

<< MemberLogin 서블릿 >>

더보기

package member;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import utility.ParameterUtil;

@WebServlet("/member/login")
public class MemberLogin extends HttpServlet {
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 파라미터 검증
		String id = ParameterUtil.getString(request, "id");
		String pw = ParameterUtil.getString(request, "pw");
		
		if(!ParameterUtil.isId(id)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isPw(pw)) {
			response.setStatus(400);
			return ;
		}
		
		MemberDto member = new MemberDto(id, pw, null, null);
		
		MemberService service = new MemberService();
		boolean result = service.correctIdNPw(member);
		
		if(!result) {
			response.setStatus(400);
		}
	}

}


로그인 페이지에 [ 로그인 ] 버튼을 구현하자

login.html에 script 만 넣으면 됨

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/login.css">
</head>
<body>
		<header>&#60;&#60; 로그인 &#62;&#62;</header>
		<main>
			<form action="/studyProject/member/login" method="POST">
				<fieldset>
					<label for="id">아이디</label>
					<input type="text" name="id" id="id" required="required" dp-name="아이디">
				</fieldset>
				<fieldset>
					<div>
						<label for="pw">비밀번호</label>
						<input type="password" name="pw" id="pw" required="required" dp-name="비밀번호">
					</div>
				</fieldset>
				<fieldset>
					<button type="button" role="submit">회원가입</button>
				</fieldset>
			</form>
		</main>
		
		<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
		<script>
			$("button[role='submit']").on("click", function() {
				let id = $("#id").val();
				let pw = $("#pw").val();
				
				$.ajax({
					url: $("form").attr("action"),
					type: $("form").attr("method"),
					data: {"id": id, "pw": pw},
					success: function() {
						location.href = "/studyProject/member/index.html";
					},
					error: function() {
						alert("아이디 또는 비밀번호를 확인해주세요.");
					}
				});
			});
		</script>
</body>
</html>


이제 서버를 시작하고 아이디, 비밀번호를 올바르게 입력도 해보고 잘못 입력도 해보면서 결과를 확인하자

728x90
LIST

<< 학습 목표 >>

1. Connection Pool을 사용해야하는 이유를 설명할 수 있다.

2. Connection Pool을 사용하도록 설정할 수 있다.

3. Connection Pool에서 Connection을 꺼내는 코드를 작성할 수 있다.


이 글에서는 Connection Pool을 사용해야하는 이유와 Connection Pool에 대해서 배울 것

 

이전 글에서 만든 회원 가입 서비스를 면밀히 분석해보자

회원 가입 서비스의 동작 순서를 시각화한 그림을 보여줄텐데 모든 상황을 시각화 하면 그림이 복잡해지므로 회원 가입에 성공하는 상황만 시각화를 해보자

 

시각화한 그림을 흐름도(flow chart) 라고 부름

흐름도에서 회원가입 컨트롤러부터 코드와 비교해서 보자

( 클릭해서 확대해서 보세요 )

 

 

파라미터 검증 부분은 흐름도에서 생략했음

클라이언트에서 보낸 회원 정보를 임시로 저장하기 위해 DTO 를 생성하고 생성과 동시에 정보를 임시로 저장하고 있음

 

 

 

회원 정보를 DB에 저장 하기 위해 서비스 객체를 생성하고 회원 가입 메서드인 join 메서드를 호출하고 있음

join 메서드를 호출하면서 저장할 회원 정보를 인자로 전달했음

( 컨트롤러 내 코드를 어디까지 봤는지 잘 기억해두자 )

 

서비스의 join 메서드를 호출했으니 이제 서비스로 넘어가서 보자

 

 

서비스의 회원가입 기능(join 메서드)에서 DB와 통신을 해야하므로 DAO 객체를 생성하고 있음

 

그 후 서비스에서는 DAO를 사용해 id 중복 여부 확인(1), 연락처 중복 여부 확인(2), 회원가입(3) 처리를 하고 있음

 

 

아이디 중복 여부를 확인하기 위해서 selectOneById 메서드를 호출하고 있음

selectOneById 메서드를 호출하면서 사용자가 입력한 아이디를 인자로 전달하고 있음

( 컨트롤러 내 어디까지 봤는지 잘 기억해야하고 서비스의 어디까지 봤는지도 잘 기억하자 )

 

DAO의 selectOneById 메서드를 호출했으니 이제 DAO로 넘어가서 보자

 

쿼리를 전송하기 위해 "DB에 접속"한 후 SELECT 쿼리를 준비하고 보낸 후 실행했음

그 다음 DB로부터 결과를 받아와 rs 객체에 저장했음

 

! 여기서 잠깐 ! 다른 것 보다 "DB에 접속" 을 강조했음

Connection Pool을 왜 사용하는지 알기 위해서는 "쿼리를 보내고 실행한 후 결과를 받아오기 위해 DB에 접속했다" 가 굉장히 중요한 포인트임

굉장히 중요한 포인트라 흐름도에 그렸어야 했지만 그림이 너무 길어져 작게 보여 어쩔 수 없이 생략했음

 

다시 DAO 로 돌아가서 id 로 조회한 결과가 있다면 if문 안으로 들어가 조회한 데이터를 member 객체(DTO)에 담음

id 로 조회한 결과가 없다면 if문을 건너뛰어 member 객체(DTO)는 그대로 null 상태임

( 이미지는 생략했음 )

 

selectOneById 메서드에서 조회 결과를 반환하기 전에 "DB와 관련된 자원을 해제" 한 후 조회 결과를 반환함

! 여기서 잠깐 ! "DB에 접속" 을 강조했던 것처럼 여기서도 "DB와 관련된 자원을 해제" 를 강조했음

"DB와 관련된 자원을 해제" 한다는건 rs.close(), pstmt.close(), conn.close() 를 하는 것인데 conn.close() 는? DB 접속을 끊는 것

역시나 Connection Pool 을 왜 사용하는지 알기 위해서는 "쿼리를 보내고 실행한 후 결과를 받아온 다음에 DB 접속을 끊었다" 가 굉장히 중요한 포인트임

굉장히 중요한 포인트라 흐름도에 그렸어야 했지만 그림이 너무 길어져 작게 보여 어쩔 수 없이 생략했음

 

강조한 부분을 정리해보면 "쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]" 임

 

 

DAO가 결과를 반환했으니 다시 서비스의 selectOnById 메서드를 호출한 부분으로 돌아가자

DAO의 selectOneById 메서드가 반환한 정보가 있다면 if 문 안으로 들어가 서비스를 빠져나가 컨트롤러에게 결과(duplicate_id) 를 전달함 / 그러나 회원 가입에 성공한 상황만 본다고 했으므로 selectOneById 메서드가 반환한 정보가 없다고 하자

 

 

그 다음 연락처 중복 여부를 확인함

연락처 중복 여부 확인부터는 중요한 포인트만 언급하겠음

연락처 중복 여부 확인은 아이디 중복 여부 확인과 마찬가지로 DAO에서 쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]

 

서비스의 마지막으로 아이디, 연락처가 중복 되지 않았을 때는 회원가입을 진행함

회원 가입 또한 마찬가지로 DAO에서 쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]

 

회원 가입에 성공했다면 서비스는 컨트롤러에게 "OK" 를 반환하고 어떤 이유로 인해 회원 가입에 실패했다면 서비스는  컨트롤러에게 "unknown error" 을 반환함

 

 

이제 컨트롤러로 돌아가서 컨트롤러는 서비스가 반환한 결과에 따라 상태 코드를 클라이언트에게 전달함

 

여기까지 다소 길었지만 우리가 앞서 만든 회원 가입 서비스의 흐름을 알아봤음

중간에도 언급했지만 우리가 회원 가입 서비스의 흐름을 알아본 이유는 Connection Pool 을 왜 사용하는지 알기 위함임


지금까지가 서론이었고 이제부터 본론으로 들어가자~!

 

한 사람이 회원가입 하기 위해 DB에 접속하고 접속을 끊은 횟수는? 3번

1. 아이디 중복 여부를 확인하기 위해

2. 연락처 중복 여부를 확인하기 위해

3. 회원 가입을 하기 위해

 

열 사람이 회원 가입을 한다면 DB에 접속하고 접속을 끊는 횟수는? 30번

서비스가 대박나 천 사람, 만 사람이 회원 가입을 한다면 DB에 접속하고 접속을 끊는 횟수는? 3,000번 30,000번

이렇게 클라이언트의 요청 한번에 DB와 접속, 접속 끊기 횟수가 굉장히 많기 때문에 DB와 접속, 접속 끊기는 최소한으로 이뤄지는게 바람직함

 

DB와 접속, 접속 끊기를 최소화 해줄 수 있는 기술이 Connection Pool 임

Connection Pool은 미리 Connection 객체(DB에 접속)를 여러 개 만들어둠

그 후 DAO에서 Connection 객체(DB에 접속)가 필요할 때 Connection Pool에 있는 Connection 객체를 가져옴

 

DAO에서는 DB에 접속 하는 과정 대신 Connection Pool에서 Connection 객체를 가져오는 과정으로 바뀜

그 이후에는 지금의 DAO와 똑같이 쿼리를 보내고 결과를 가져옴

그리고 마지막으로 DB와 관련된 모든 자원들을 close 함

이때 conn.close() 를 해서 접속을 끊을텐데 Connection Pool 에서 가져온 Connection 객체를 close() 하게 되면 접속을 끊는게 아니라 가져왔던 Connection 객체를 Connection Pool에 반납하는 과정으로 바뀜

 

이전까지 DAO의 과정과 지금부터 사용할 Connection Pool을 사용한 DAO의 과정을 시각적으로 보자

 

이전까지 DAO에서는 쿼리 하나를 전송 & 실행 & 결과 받기 를 하기 위해 접속, 끊기를 해야함

이전까지 DAO에서는 쿼리를 여러 번 전송 & 실행 & 결과 받기 를 하기 위해 접속, 끊기를 여러 번 해야함

 

Connection Pool을 사용한 DAO에서는 [ 쿼리 하나를 전송 & 실행 & 결과 받기 ] 를 하기 위해 접속, 끊기를 하지 않음

Connection Pool을 사용한 DAO에서는 [ 쿼리를 여러 번 전송 & 실행 & 결과 받기 ] 를 하기 위해 접속, 끊기를 하지 않음

접속 대신 Connection Pool에서 Connection 객체 가져오기를 함

끊기 대신 Connection 객체를 Connection Pool에 반납 함


Connection Pool은 톰캣이 제공하는 것이라 위 흐름도에서 Connection Pool에 톰캣의 이미지를 넣어놨음

톰캣에 Connection Pool 관련된 설정을 해두면 서버가 실행될 때 설정에 따라 Connection 객체(DB에 접속)을 생성함

따라서 이제는 서버가 실행될 때 여러 번의 DB 접속이 이뤄짐

그에 따라 서버가 실행될 때는 서버와 DB에 다소 부담이 생길 수 있음

그러나 서버가 완전히 실행되고 나서는 몇 번의 쿼리를 보내든 서버와 DB에 부담이 없음

( 완전히 없지는 않고 Connection Pool을 사용하기 전 보다 상당히 완화 되었음 )

 

이제 DAO가 Connection Pool 을 사용하기 위해서 톰캣에 Connection Pool 관련된 설정을 하자

톰캣에 Connection Pool 관련된 설정을 하려면 우선 DBCP 라이브러리를 다운 받아야함

DBCP는 DataBase Connection Poo의 약자임

 

라이브러리를 다운 받기 위해 구글에서 maven repositry 검색 후 첫 번째 검색 결과(1)로 들어가자

( 만약 검색 결과가 이와 다르다면 직접 들어가자 / https://mvnrepository.com/ )

 

 

tomcat dbcp 로 검색(1) 후 첫 번째 검색 결과로 들어가자(2)

( 만약 검색 결과가 이와 다르다면 직접 들어가자 / https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp )

 

적당한 버전을 선택해 들어가자(1)

 

 DBCP 라이브러리가 압축되어있는  jar 형식 압축 파일을 다운 받자(1)

 

DBCP 라이브러리 추가의 마지막으로 다운로드 받은 jar 형식의 라이브러리를 프로젝트 -> src -> main -> webapp -> WEB-INF -> lib 로 드래그 & 드롭하자

 

여기까지 라이브러리 추가 완료

이제 톰캣에 Connection Pool 관련 설정을 하자

톰캣에 Connection Pool 관련 설정을 할 때는 context.xml(1) 에 해야함

 

간혹 인터넷에서 보면 프로젝트 -> WEB-INF -> web.xml 에 Connection Pool 관련된 설정을 하는 글들이 있는데 context.xml 대신 web.xml에 Connection Pool 설정을 하기도 하지만 web.xml에 Connection Pool을 설정하면 해당 프로젝트에서만 Coonection Pool 을 사용할 수 있음

( "이게 무슨 차이지?" 싶은 분들은 https://codingaja.tistory.com/17 이 글로 가서 [ 웹 서버 안에는 하나 또는 다수의 다양한 웹 서비스가 실행될 수 있음 ] 부분까지 읽어보고 오자 )

 

- context.xml 에 Connection Pool 설정을 한다 => 서버 단위로 Connection Pool 설정을 하므로 해당 서버 내 모든 웹 프로젝트들이 Connection Pool을 사용할 수 있음

- web.xml 에 Connection Pool 설정을 한다 => 웹 프로젝트 단위로 Connection Pool 설정을 하므로 해당 서버 내 웹 프로젝트 마다 web.xml에 Connection Pool 설정을 해줘야함

 

다시 Connection Pool 설정으로 돌아와서 context.xml 에 Connection Pool 설정을 하자

context.xml 의 가장 마지막인 </Context> 의 바로 위에 Resource 태그를 추가하자

Resource 태그가 Connection Pool 설정을 하는 태그

 

name 속성
Connection Pool의 이름
auth 속성
인증 주체 ( 정확한 용도는 잘 모르겠음 )
type 속성
Connection Pool의 데이터 타입
driverClassName 속성
사용하는 DB 마다 정해진 driver 이름 / mariadb가 아닌 DB를 사용하는 분들은 해당 DB에 맞는 driver 이름을 입력하면 됨
url 속성
DB에 접속하기 위한 URL
username 속성
DB에 접속하기 위한 사용자 아이디
password 속성
DB에 접속하기 위한 사용자 비밀번호
maxTotal 속성
동시에 사용할 수 있는 최대 Connection 개수
간단하게 서버가 시작되면 Connection Pool에서는 Connection 객체가 50개 생성된다고 생각하면 됨
톰캣 서버와 DB 서버의 사양을 고려해 적절한 값으로 설정함

취업 준비를 할 때는 마음대로 갯수를 설정해도 되지만 취업을 하고 나서 maxTotal 속성을 건드릴 일이 있다면 상사에게 물어보면 됨
maxIdle 속성
Connection Pool에 반납할 때 최대로 유지될 수 있는 Connection 개수
maxWaitMillis 속성
Connection Pool에서 Connection을 꺼내려고 하는데 모든 Connection이 사용중이여서 꺼낼 Connection이 없을 때 최대로 기다릴 시간

maxTotal 속성을 사용해서 DAO들이 사용할 Connection을 준비했는데 DAO들이 처리할 작업이 복잡해 Connection을 오래 사용하는 상황이라 Connection Pool에 있는 모든 Connection을 빌려갔는데 또 다른 DAO에서 Connection Pool에 있는 Connection을 꺼내려고 하면 Connection Pool에서는 빌려줄 Connection 이 없음

이때 DAO는 maxWaitMillis 만큼 기다리면서 Connection Pool이 Connection을 빌려주기를 기다림

maxWaitMillis 속성의 값은 밀리초 단위로 지정하며 1000 으로 하면 DAO가 Connection을 빌려주기를 1초 동안 기다린다는 것
이 속성의 기본값은 -1 로 -1은 DAO가 Connection을 빌려주기를 무한정 기다린다는 것 ( 빌려줄 때까지 계속 대기하겠다는 것 )

취업 준비를 할 때는 -1 로 설정해도 상관 없지만 취업을 하고 나서 maxWaitMillis 속성을 건드릴 일이 있다면 상사에게 물어보면 됨

Connection Pool 설정과 관련된 속성은 이외에도 더 많으니 Coonection Pool 설정을 더 세세하게 하고 싶다면 직접 찾아보자

Connection Pool 과 관련된 제대로된 이해가 필요하다면 https://d2.naver.com/helloworld/5102792 이 블로그의 글을 보자

 

다시~! 본론으로 돌아와서 잠깐 지금까지 한 것들을 정리하자

1. DAO에서 Connection Pool을 사용하기 위해 DBCP 라이브러리를 추가했음

2. DAO에서 Connection Pool을 사용하기 위해 context.xml 에 Connection Pool 설정을 했음

 

여기까지 DAO에서 Connection Pool을 사용하기 위한 설정이 끝났으니 이제 DAO에서 Connection Pool을 사용하자

DAO에서 Connection Pool을 사용하기 위해서는 DAO 자체의 코드를 바꿀 필요는 없음

왜? 우리는 앞서 DBUtil 클래스에 getConnection 메서드를 선언해 DAO에서 사용할 Connection을 생성해 반환하도록 했으므로...

따라서~! 우리가 바꿔야하는건 DBUtil 클래스의 getConnection 메서드 뿐!

DBUitl 클래스의 getConnection 메서드에서 Connection Pool 을 가져온 뒤 Connection Pool 에서 Connection을 꺼내 반환하면 됨

아래 코드를 DBUtil 클래스의 getConnection 메서드에 입력하자

( Connection을 위해 선언해뒀던 멤버 변수들은 모두 지웠음 )

package utility;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

public class DBUtil {
	public static Connection getConnection() {
		Connection conn = null;
		
		try {
			Context ctx = new InitialContext();
			Context envCtx = (Context) ctx.lookup("java:/comp/env");
			DataSource ds = (DataSource) envCtx.lookup("jdbc/mariadb/cp1");
			
			conn = ds.getConnection();
		} catch (NamingException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return conn;
	}
    
    ...
}

Connection Pool을 가져오기 위해서는 JNDI ( Java Naming and Directory Interface ) 를 사용해야함

톰캣이 실행될 때 Connection Pool 설정이 되어있다면 설정에 따라 Connection Pool 을 생성한 뒤 JNDI 에 Connection Pool을 저장해둠

 

JNDI 를 가져오기 위해 객체를 생성(1) 한 후 JNDI 를 가져옴(2)

그 후 JNDI 에서 jdbc/mariadb/cp1 이라는 이름의 값을 꺼낸 후(3) Connection Pool 로 형변환(4) 했음

 

JNDI 에서 값을 꺼낼 때 사용한 이름은 우리가 Resource 태그에 지정한 name 속성임

Connection Pool 로 형변환이 (Connection Pool) 이 아니라 (DataSource) 인 이유는 Resource 태그에 지정한 Connection Pool의 타입을 DataSource 로 지정했기 때문임

 

여기까지 DAO가 Connection Pool을 사용하기 위한 설정을 하고 DAO가 Connection Pool을 사용하도록 코드를 바꿨음

 

close를 하는 메서드는 바꿀 필요가 없음

getConnection 메서드의 try 의 마지막에서 conn = ds.getConnection() 을 하면서 Connection Pool에서 Conncetion을 꺼냈으므로 close 메서드의 내부 동작이 Connection Pool 로 반납하도록 바뀌었음


여기까지 Connection Pool을 사용해야하는 이유와 Connection Pool을 사용하도록 바꿨음

 

DB 관련되서 더 깊게 들어가 실무에 가까운 코드를 생각하고 있다면 DB 과목의 트랜젝션(Transaction) 을 공부해보자

728x90
LIST

<< 학습 목표 >>

1. CRUD를 설명할 수 있다.

2. 회원가입 서비스를 개발할 수 있다.


여기까지 서블릿의 2 / 3 을 배웠음

앞으로 남은건 Chapter5(쿠키, 세션), Chapter6(필터와 리스너)임

다음 챕터로 넘어가기 전에 지금까지 배운것들로 간단하게 프로젝트를 진행하자

 

세상에 있는 모든 서비스는 CRUD 라고 부르는 구성요소로 이루어져있음

C - Create

R - Read

U - Update

D - Delete

쿠팡, 네이버 같은 웹 서비스는 물론이고 게임도 카카오톡과 같은 앱도 CRUD 구성요소로 이루어져있음

CRUD 구성요소가 없는 서비스라거나 CRUD 중 무언가가 빠져있는 서비스는 없음

 

이제 우리도 서블릿을 거의 다 배워가니 서비스 다운 서비스 하나를 만들어보자

우리가 만들 서비스는 모든 서비스의 기초가 되는 회원가입, 로그인, 회원정보수정, 회원탈퇴 를 가지고 있는 서비스임

이를 통해 자바, 서블릿을 더 깊게 이해하고 이 서비스를 잘 복습하면 여러분이 만들고 싶은 서비스를 만들 수도 있을 것


먼저 이 글에서는 회원가입 서비스를 만들 것

회원 가입이란 사용자가 "회원 가입 화면에 입력한 정보(회원 정보)"를 DB에 저장하는 것을 말함

회원 가입은 C에 해당함 / Create는 "만들다" 라는 뜻을 가지고 있음

사용자가 회원 가입을 하면 우리 서비스에 없었던 회원 정보가 생성되는 것이므로 C에 해당함

 

어떤 서비스를 만들기 위해서는 항상 기획 -> 개발 -> 테스트 -> 완료 의 과정을 거침

우리는 간단한 서비스이므로 기획은 필요 없지만 일반적으로 기획 할 때는 "개발할 기능에 대한 정의" 와 "스토리보드(와이어프레임)", "인터페이스" 가 반드시 필요함

 

스토리보드란 내가 만들 서비스의 화면, 서비스를 이용하는 예상 시나리오를 작성하는 것

사용자가 스토리보드를 봤을 때 "내가 이런 기능을 사용하게 되는구나" 라고 명확히 알 수 있어야함

 

인터페이스란 내가 만들 서비스의 서블릿에 대한 계획임

회원 가입 화면에서 [ 회원 가입 ] 버튼을 누르면 회원 가입 서블릿이 동작해 회원 가입 요청을 받아 회원 가입 처리를 할 것

이때 이 서블릿을 어떻게 만들지에 대한 계획으로 URL, 요청 방식, 파라미터, 상태 코드 등을 기입함

 

<< 기능에 대한 정의 >>
로그인 하지 않은 사용자가 사용할 수 있는 기능으로 아이디, 비밀번호, 비밀번호 확인, 닉네임, 연락처를 입력한 후 회원 가입을 함

아이디, 연락처는 중복 되지 않아야함
나이가 어린 사용자의 경우 핸드폰이 없어 연락처가 없을 수 있으므로 연락처는 없을 수도 있음

아이디는 영문 대소문자, 숫자로 이루어져있고 영문 대소문자로 시작해야함
또한 아이디는 최소 3자, 최대 20자까지 가능함

비밀번호는 영문 대소문자, 숫자, 특수문자(!, @, #, $, %, ^, &, *)로 이루어져있고 최소 6자, 최대 16자까지 가능함

닉네임은 한글, 영문 대소문자, 숫자로 이루어져있고 숫자로 시작할 수 없음
또한 닉네임은 최소 2자, 최대 10자까지 가능함

연락처는 010-1111-1111과 같은 형식이고 01X 로 시작하는 번호는 입력할 수 없음
( 참고, https://www.ajunews.com/view/20210309141213275 )
<< 스토리 보드 >>
<< 인터페이스 >>

 

회원 정보를 저장할 테이블을 만들자

테이블은 practice DB에 추가함

CREATE TABLE `member` (
`idx` INT(11) NOT NULL AUTO_INCREMENT,
`id` VARCHAR(20) NOT NULL,
`pw` VARCHAR(16) NOT NULL,
`nickname` VARCHAR(10) NOT NULL,
`tel` CHAR(13) NOT NULL,
`joinDateTime` TIMESTAMP NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`idx`),
UNIQUE INDEX `id` (`id`),
UNIQUE INDEX `tel` (`tel`)
)

이 카테고리에서는 HTML, CSS, JS 의 기초 내용은 다루지 않으므로 웹 페이지는 별도 설명 없이 코드만 첨부함

 

먼저 JS 없이 HTML, CSS 만을 사용한 화면을 만들자

우선 공용 CSS 부터 보자

webapp 안에 css 폴더 만들고 css 폴더 안에 public 폴더를 만들자

public 폴더 안에 minireset.css 를 만들고 아래 코드를 추가하자

더보기

@charset "UTF-8";
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
html,
body,
p,
ol,
ul,
li,
dl,
dt,
dd,
blockquote,
figure,
fieldset,
legend,
textarea,
pre,
iframe,
hr,
h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  padding: 0;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: 100%;
  font-weight: normal;
}

ul {
  list-style: none;
}

button,
input,
select {
  margin: 0;
}

html {
  box-sizing: border-box;
}

*, *::before, *::after {
  box-sizing: inherit;
}

img,
video {
  height: auto;
  max-width: 100%;
}

iframe {
  border: 0;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

td,
th {
  padding: 0;
}

fieldset {
	border: 0;
}

 

webapp -> css -> public -> common.css 를 추가하고 아래 코드를 추가하자

더보기

@charset "UTF-8";
@import "minireset.css";

header {
	display: flex;
	justify-content: center;
	padding: 20px;
}

main {
	padding: 20px;
	padding-top: 0;
	display: flex;
	justify-content: center;
}


이번에는 회원 가입 페이지를 만들자

webapp 폴더 안에 member 폴더를 만들고 그 안에 join.html 을 추가한 뒤 아래 코드를 추가하자

<< 회원 가입 HTML >>

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/join.css">
</head>
<body>
	<header>&#60;&#60; 회원 가입 &#62;&#62;</header>
	<main>
		<form action="/studyProject/member/join" method="POST">
			<fieldset>
				<label for="id">아이디</label>
				<input type="text" name="id" id="id">
			</fieldset>
			
			<fieldset>
				<div>
					<label for="pw">비밀번호</label>
					<input type="password" name="pw" id="pw">
				</div>
				<div>
					<label for="pwchk">비밀번호 확인</label>
					<input type="password" name="pw" id="pwchk">
				</div>
			</fieldset>
			
			<fieldset>
				<label for="nickname">닉네임</label>
				<input type="text" name="nickname" id="nickname">
			</fieldset>
			
			<fieldset>
				<label for="tel">연락처</label>
				<input type="tel" name="tel" id="tel">
			</fieldset>
			<fieldset>
				<button type="button">회원가입</button>
			</fieldset>
		</form>
	</main>
</body>
</html>

 

webapp -> css -> member 폴더를 만들고 그 안에 join.css 를 추가한 뒤 아래 코드를 추가하자

<< 회원 가입 CSS >>

더보기

@charset "UTF-8";

fieldset {
	margin-bottom: 10px;
}

body > main > form > fieldset:nth-child(5) {
	display: flex;
	justify-content: end;
}

label {
	display: inline-block;
	width: 102px;
	text-align: right;
}

 

webapp -> member -> joinSuccess.html 을 추가한 뒤 아래 코드를 추가하자

<< 회원가입 완료 HTML >>

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/joinSuccess.css">
</head>
<body>
	<header>&#60;&#60; 회원 가입 완료 &#62;&#62;</header>
	<main>
		<div id="content_wrapper">
			<div id="image_wrapper">
				<div><img src="/studyProject/images/signupSuccess_left.png"></div>
				<div><img src="/studyProject/images/signupSuccess_right.png"></div>
			</div>
			
			<a href="#">로그인 페이지로 이동</a>
		</div>
	</main>
</body>
</html>

 

webapp -> css -> member -> joinSuccess.css 를 추가 하고 아래 코드를 추가하자

<< 회원 가입 완료 CSS >>

더보기

@charset "UTF-8";

#content_wrapper {
	text-align: center;
}

#image_wrapper {
	display: flex;
}

img {
	width: 100px;
}


<< 회원 가입 완료 이미지 파일 >>

이미지 파일은 webapp -> images 폴더를 만들고 그 안에 넣어두자

signupSuccess_left.png
0.21MB
signupSuccess_right.png
0.22MB

 

페이지를 만들었으니 한번씩 확인하자


이제 서블릿(기능) 을 만들 차례

그 전에 파라미터를 한 차례 검증 후 꺼내줄 메서드를 추가하자

혹시 "파라미터 검증이 뭐지?" 라고 생각이 드시는 분은 https://codingaja.tistory.com/24 이 글을 읽고 오자

util 패키지 -> ParameterUtil 클래스를 추가한 후 아래 코드를 추가하자

더보기

package utility;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

public class ParameterUtil {
	/**
	 * name 파라미터 값을 꺼내는 메서드
	 * 파라미터가 없거나 파라미터 값을 trim() 한 후 비어있다면 null을 반환
	 * 
	 * @param request 파라미터를 담고 있는 객체
	 * @param name 값을 꺼낼 파라미터의 이름
	 * @return String 또는 null
	 */
	public static String getString(HttpServletRequest request, String name) {
		String str = null;
		
		str = request.getParameter(name);
		
		if(str != null) {
			str = str.trim();
			
			if(str.length() == 0) {
				str = null;
			}
		}
		
		return str;
	}
	
	public static boolean isId(String id) {
		String pattern = "[a-zA-Z0-9]{3,20}";
		
		Pattern p = Pattern.compile(pattern);
		Matcher m = p.matcher(id);
		
		return m.matches();
	}
	
	public static boolean isPw(String pw) {
		String pattern = "(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}";
		
		Pattern p = Pattern.compile(pattern);
		Matcher m = p.matcher(pw);
		
		return m.matches();
	}
	
	public static boolean isNickname(String nickname) {
		String pattern = "[가-힣a-zA-Z0-9]{2,10}";
		
		Pattern p = Pattern.compile(pattern);
		Matcher m = p.matcher(nickname);
		
		return m.matches();
	}
	
	public static boolean isTel(String tel) {
		String pattern = "010-([0-9]{4})-([0-9]{4})";
		
		Pattern p = Pattern.compile(pattern);
		Matcher m = p.matcher(tel);
		
		return m.matches();
	}
}

 

이제 회원 가입 기능을 만들자

회원 가입 기능을 만들 때는 인터페이스를 참고해야함

혹시 "왜?" 라는 생각이 들면 다시 이 글을 처음부터 읽고 오자

 

회원가입 인터페이스를 추가하기 위해 member 패키지를 추가하자

member 패키지 안에 MemberJoin 서블릿을 추가하고 우선 회원가입 인터페이스대로 대략적으로만 구성해놓자

더보기

package member;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import utility.ParameterUtil;

@WebServlet("/member/join")
public class MemberJoin extends HttpServlet {
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 파라미터 검증
		String id = ParameterUtil.getString(request, "id");
		String pw = ParameterUtil.getString(request, "pw");
		String nickname = ParameterUtil.getString(request, "nickname");
		String tel = ParameterUtil.getString(request, "tel");
		
		if(!ParameterUtil.isId(id)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isPw(pw)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isNickname(nickname)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isTel(tel)) {
			response.setStatus(400);
			return ;
		}
		
		// 회원 가입
		
		if(회원가입에 성공했다) {
			// 상태 코드를 설정하지 않으면 자동으로 200으로 설정되므로
			// 설정 생략
//			response.setStatus(200);
		} else if(아이디 또는 연락처가 중복됐다) {
			response.setStatus(409);
		} else {
			response.setStatus(500);
		}
	}
}

 

앞서 언급했듯 서블릿은 컨트롤러(Controller / 제어자)의 역할임

컨트롤러의 역할은 다음과 같음

1. 클라이언트의 요청을 받는다.

2. 파라미터 검증을 한다.

3. 요청에 맞는 처리할 서비스를 호출한다.

4. 서비스가 반환한 결과에 따라 적절한 결과를 클라이언트에게 전달한다.

 

우리는 지금 1, 2, 4 를 가지고 있는 서블릿을 만들었음

3은 회원가입임

 

회원 가입할 서비스를 만들자

서비스는 자바 코드임

자바 코드를 다른 말로 POJO(Plain Old Java Object) 라고 부름

member 패키지에 MemberService 클래스를 추가하고 아래 코드를 입력하자

더보기

package member;

public class MemberService {
	public String join(String id, String pw, String nickname, String tel) {
		// 아이디 중복 여부 확인
		if(아이디가 중복되었다) {
			return "duplicate_id";
		}
		
		// 연락처 중복 여부 확인
		if(연락처가 중복되었다) {
			return "duplicate_tel";
		}
		
		// 회원가입
		
	}
}

 

! 여기서 잠깐 ! 회원가입 서비스(join 메서드)가 가입할 회원 정보를 전달 받기 위해 4개의 매개변수를 사용하고 있음이를 더 효율적으로 바꾸자자바에서 정보를 저장할 때는 DTO 또는 VO 라고 부르는 자바 코드를 사용함DTO는 Data Transfer Object, VO는 ValueObject 의 약자로 정보를 담아 전달하기 위한 용도인 클래스를 말함컨트롤러에서 회원 정보를 담아 서비스로 전달하기 위해 memberDto 를 추가하자member 패키지에 memberDto 클래스를 추가하고 아래와 같이 코드를 입력하자

더보기

package member;

import java.time.LocalDateTime;

public class MemberDto {
	private int idx;
	private String id;
	private String pw;
	private String nickname;
	private String tel;
	private LocalDateTime joinDateTime;

	public MemberDto(int idx, String id, String pw, String nickname, String tel, LocalDateTime joinDateTime) {
		this.idx = idx;
		this.id = id;
		this.pw = pw;
		this.nickname = nickname;
		this.tel = tel;
		this.joinDateTime = joinDateTime;
	}

	public MemberDto(String id, String pw, String nickname, String tel) {
		this.id = id;
		this.pw = pw;
		this.nickname = nickname;
		this.tel = tel;
	}

	public int getIdx() {
		return idx;
	}

	public void setIdx(int idx) {
		this.idx = idx;
	}

	public String getId() {
		return id;
	}

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

	public String getPw() {
		return pw;
	}

	public void setPw(String pw) {
		this.pw = pw;
	}

	public String getNickname() {
		return nickname;
	}

	public void setNickname(String nickname) {
		this.nickname = nickname;
	}

	public String getTel() {
		return tel;
	}

	public void setTel(String tel) {
		this.tel = tel;
	}

	public LocalDateTime getJoinDateTime() {
		return joinDateTime;
	}

	public void setJoinDateTime(LocalDateTime joinDateTime) {
		this.joinDateTime = joinDateTime;
	}
}

 

이제 컨트롤러와 서비스 코드에서 MemberDto 를 사용하도록 바꾸자<< MemberJoin 서블릿 >>

더보기

package member;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import utility.ParameterUtil;

@WebServlet("/member/join")
public class MemberJoin extends HttpServlet {
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 파라미터 검증
		String id = ParameterUtil.getString(request, "id");
		String pw = ParameterUtil.getString(request, "pw");
		String nickname = ParameterUtil.getString(request, "nickname");
		String tel = ParameterUtil.getString(request, "tel");
		
		if(!ParameterUtil.isId(id)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isPw(pw)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isNickname(nickname)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isTel(tel)) {
			response.setStatus(400);
			return ;
		}
		
		// 회원 가입
		MemberDto member = new MemberDto(id, pw, nickname, tel);
		
		MemberService service = new MemberService();
		String result = service.join(member);
		
		if(회원가입에 성공했다) {
			// 상태 코드를 설정하지 않으면 자동으로 200으로 설정되므로
			// 설정 생략
//			response.setStatus(200);
		} else if(아이디 또는 연락처가 중복됐다) {
			response.setStatus(409);
		} else {
			response.setStatus(500);
		}
	}
}

<< MemberService >>

더보기

package member;

public class MemberService {
	public String join(MemberDto member) {
		// 아이디 중복 여부 확인
		if(아이디가 중복되었다) {
			return "duplicate_id";
		}
		
		// 연락처 중복 여부 확인
		if(연락처가 중복되었다) {
			return "duplicate_tel";
		}
		
		// 회원가입
		
	}
}


다시 서비스로 돌아가서 MemberService의 join 메서드가 서비스로 회원가입을 하기 위해 아이디 중복 여부 확인, 연락처 중복 여부 확인을 함

아이디, 연락처 모두 중복되지 않았다면 회원가입을 함

아이디, 연락처 중복 여부는 DB의 member 테이블에서 사용자가 입력한 아이디 또는 연락처와 같은 계정 정보가 있는지 SELECT 를 하면 됨

사용자가 입력한 아이디 또는 연락처와 같은 계정 정보가 있다면 중복되는 것이고 같은 계정 정보가 없다면 중복되지 않은 것임

회원가입은 회원 정보를 DB의 member 테이블에 INSERT 하면 됨

DB와 통신하는 서비스가 필요하므로 DAO 를 만들자

 

DAO 전에 DAO를 위해 utility 패키지에 DBUtil 클래스를 추가하고 아래 코드를 입력하자

더보기

package utility;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtil {
	private static final String DRIVER_NAME = "org.mariadb.jdbc.Driver";
	private static final String DB_URL = "jdbc:mariadb://localhost:3306/practice?user=root&password=0000";
	
	public static Connection getConnection() {
		Connection conn = null;
		
		try {
			Class.forName(DRIVER_NAME);
			
			conn = DriverManager.getConnection(DB_URL);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return conn;
	}
	
	private static void close(Connection conn) {
		if(conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
	
	private static void close(PreparedStatement pstmt) {
		try {
			if(pstmt != null) {
				pstmt.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	private static void close(ResultSet rs) {
		try {
			if(rs != null) {
				rs.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	public static void closes(Connection conn, PreparedStatement pstmt) {
		closes(conn, pstmt, null);
	}
	
	public static void closes(Connection conn, PreparedStatement pstmt, ResultSet rs) {
		close(rs);
		close(pstmt);
		close(conn);
	}
}

 

member 패키지에 MemberDao 클래스를 추가하고 아래 코드를 추가하자

더보기

package member;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;

import utility.DBUtil;

public class MemberDao {
	public MemberDto selectOneById(String id) {
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		
		MemberDto member = null;
		
		try {
			conn = DBUtil.getConnection();
			
			String sql = "SELECT * FROM member WHERE id = ?";
			
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, id);
			
			rs = pstmt.executeQuery();
			
			if(rs.next()) {
				int idx = rs.getInt("idx");
				String pw = rs.getString("pw");
				String nickname = rs.getString("nickname");
				String tel = rs.getString("tel");
				LocalDateTime joinDateTime = rs.getTimestamp("joinDateTime").toLocalDateTime();
				
				member = new MemberDto(idx, id, pw, nickname, tel, joinDateTime);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			DBUtil.closes(conn, pstmt, rs);
		}
		
		return member;
	}
	
	public MemberDto selectOneByTel(String tel) {
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		
		MemberDto member = null;
		
		try {
			conn = DBUtil.getConnection();
			
			String sql = "SELECT * FROM member WHERE tel = ?";
			
			pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, tel);
			
			rs = pstmt.executeQuery();
			
			if(rs.next()) {
				int idx = rs.getInt("idx");
				String id = rs.getString("id");
				String pw = rs.getString("pw");
				String nickname = rs.getString("nickname");
				LocalDateTime joinDateTime = rs.getTimestamp("joinDateTime").toLocalDateTime();
				
				member = new MemberDto(idx, id, pw, nickname, tel, joinDateTime);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			DBUtil.closes(conn, pstmt, rs);
		}
		
		return member;
	}
}

 

우선 이 DAO 에는 회원 가입은 빼고 아이디 중복 여부를 확인하기 위한 메서드와 연락처 중복 여부를 확인하기 위한 메서드를 넣었음

위에서 언급한대로 selectOneById 메서드는 아이디로 계정 정보를 조회하고 조회된 결과가 있다면 반환함

selectOneByTel 메서드는 연락처로 계정 정보를 조회하고 조회된 결과가 있다면 반환함

 

서비스에서는 이 두 메서드를 사용해서 두 메서드가 반환한 계정 정보가 있다면 아이디 또는 연락처가 중복된 것으로 판단하고 반환한 계정 정보가 없다면 아이디 또는 연락처가 중복된 것이 아닌것으로 판단하면 됨

<< MemberService 클래스 >>

더보기

package member;

public class MemberService {
	public String join(MemberDto member) {
		MemberDao dao = new MemberDao();
		
		// 아이디 중복 여부 확인
		if(dao.selectOneById(member.getId()) != null) {
			return "duplicate_id";
		}
		
		// 연락처 중복 여부 확인
		if(dao.selectOneByTel(member.getTel()) != null) {
			return "duplicate_tel";
		}
		
		// 회원가입
		
	}
}

 



서비스에서 회원 가입할 때 사용할 INSERT 쿼리를 DAO에 추가하자

<< MemberDao 클래스 >>

더보기

DAO 안에 추가될 메서드만 복사해왔으니 memberDao 클래스 내 적절한 위치에 붙여넣자

public int insertByMemberDto(MemberDto member) {
	Connection conn = null;
	PreparedStatement pstmt = null;
	
	int count = 0;
	
	try {
		conn = DBUtil.getConnection();
		
		String sql = "INSERT INTO member(id, pw, nickname, tel) VALUES(?, ?, ?, ?)";
		
		pstmt = conn.prepareStatement(sql);
		pstmt.setString(1, member.getId());
		pstmt.setString(2, member.getPw());
		pstmt.setString(3, member.getNickname());
		pstmt.setString(4, member.getTel());
		
		count = pstmt.executeUpdate();
	} catch (SQLException e) {
		e.printStackTrace();
	} finally {
		DBUtil.closes(conn, pstmt);
	}
	
	return count;
}

 

서비스에 회원가입 과정을 마무리 하자

더보기

package member;

public class MemberService {
	public String join(MemberDto member) {
		MemberDao dao = new MemberDao();
		
		// 아이디 중복 여부 확인
		if(dao.selectOneById(member.getId()) != null) {
			return "duplicate_id";
		}
		
		// 연락처 중복 여부 확인
		if(dao.selectOneByTel(member.getTel()) != null) {
			return "duplicate_tel";
		}
		
		// 회원가입
		if(dao.insertByMemberDto(member) == 1) {
			return "OK";
		} else {
			return "unknown error";
		}
	}
}

 

정상적으로 회원 가입을 했다면 insertByMemberDto 메서드가 1을 반환함

따라서 회원가입의 if문을 해석해보면 "회원가입이 됐다면 OK 를 반환한다." 가 됨

else문은 "회원가입에 실패했다면 unknown error를 반환한다." 가 됨

 

이제 컨트롤러에서 서비스가 반환한 결괏값을 사용해 적절한 결괏값을 클라이언트에게 전달하도록 마무리하자

더보기

package member;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import utility.ParameterUtil;

@WebServlet("/member/join")
public class MemberJoin extends HttpServlet {
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 파라미터 검증
		String id = ParameterUtil.getString(request, "id");
		String pw = ParameterUtil.getString(request, "pw");
		String nickname = ParameterUtil.getString(request, "nickname");
		String tel = ParameterUtil.getString(request, "tel");
		
		if(!ParameterUtil.isId(id)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isPw(pw)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isNickname(nickname)) {
			response.setStatus(400);
			return ;
		} else if(!ParameterUtil.isTel(tel)) {
			response.setStatus(400);
			return ;
		}
		
		// 회원 가입
		MemberDto member = new MemberDto(id, pw, nickname, tel);
		
		MemberService service = new MemberService();
		String result = service.join(member);
		
		if(result.equals("OK")) {
			// 상태 코드를 설정하지 않으면 자동으로 200으로 설정되므로
			// 설정 생략
//			response.setStatus(200);
		} else if(result.startsWith("duplicate")) {
			response.setStatus(409);
		} else {
			response.setStatus(500);
		}
	}
}


여기까지 백엔드 구현이 완료됐음

 

이제 서버를 실행시키고 회원 가입 페이지에서 회원 가입을 해봐야할 차례인데!!

회원 가입 페이지에서 [ 회원가입 ] 버튼을 아무리 눌러도 반응이 없음

왜? [ 회원가입 ] 버튼의 HTML 태그를 보면 button 타입이기 때문에

[ 회원가입 ] 버튼이 submit 타입이라면 서버로 id, pw, pwchk, nickname, tel 을 보내 회원 가입 요청이 들어가겠지만 요청 결과로 서버는 상태 코드만 설정해 전달함

그래서 [ 회원가입 ] 버튼이 submit 타입이라면 사용자는 [ 회원가입 ] 버튼을 눌렀을 때 흰 화면만 보게됨

 

[ 회원 가입 ] 버튼의 타입을 button 타입으로 해 JS에서 Jquery의 ajax 를 사용해 회원 가입 요청을 보내고 결과를 받도록 할 것

서버가 보낸 결과가 409(아이디 또는 연락처가 중복)라면 "아이디 또는 연락처가 중복되었습니다" 를 출력할 것

서버가 보낸 결과가 200(회원 가입 성공)이라면 로그인 성공 페이지로 이동할 것

 

<< 회원 가입 HTML >>

더보기

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>연습 프로젝트</title>
	<link rel="stylesheet" type="text/css" href="/studyProject/css/public/common.css">
	<link rel="stylesheet" type="text/css" href="/studyProject/css/member/join.css">
</head>
<body>
	<header>&#60;&#60; 회원 가입 &#62;&#62;</header>
	<main>
		<form action="/studyProject/member/join" method="POST">
			<fieldset>
				<label for="id">아이디</label>
				<input type="text" name="id" id="id" required="required" dp-name="아이디">
			</fieldset>
			
			<fieldset>
				<div>
					<label for="pw">비밀번호</label>
					<input type="password" name="pw" id="pw" required="required" dp-name="비밀번호">
				</div>
				<div>
					<label for="pwchk">비밀번호 확인</label>
					<input type="password" name="pw" id="pwchk" required="required" dp-name="비밀번호 확인">
				</div>
			</fieldset>
			
			<fieldset>
				<label for="nickname">닉네임</label>
				<input type="text" name="nickname" id="nickname" required="required" dp-name="닉네임">
			</fieldset>
			
			<fieldset>
				<label for="tel">연락처</label>
				<input type="tel" name="tel" id="tel" required="required" dp-name="연락처">
			</fieldset>
			<fieldset>
				<button type="button" role="submit">회원가입</button>
			</fieldset>
		</form>
	</main>
	
	<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
	<script>
		$("button").on("click", function() {
			let id = $("#id").val();
			let pw = $("#pw").val();
			let pwchk = $("#pwchk").val();
			let nickname = $("#nickname").val();
			let tel = $("#tel").val();
			
			if(pw != pwchk) {
				alert("비밀번호와 비밀번호 확인이 일치하지 않습니다.");
				$("#pwchk").focus();
				return false;
			}
			
			$.ajax({
				url: "/studyProject/member/join",
				type: "POST",
				data: {"id": id, "pw": pw, "nickname": nickname, "tel": tel},
				success: function() {
					location.href = "/studyProject/member/joinSuccess.html";
				},
				error: function(response) {
					if(response.status == 400) {
						alert("모든 정보를 입력해주세요.");
					} else if(response.status == 409) {
						alert("아이디 또는 연락처가 중복되었습니다.");
					} else {
						alert("서버에 문제가 생겼습니다.\n잠시 후 다시 시도해주세요.");
					}
				}
			});
		});
	</script>
</body>
</html>

 

이제 서버를 실행하고 아이디, 비밀번호, 비밀번호 확인, 닉네임, 연락처 규칙을 잘 확인하고 입력 후 가입을 해보자

 

여기까지~!

길고 길었던 회원 가입 서비스를 만들어봤음

회원 가입 서비스 하나를 만들기 위해 이렇게 많은 과정을 거쳐야하고 많은 것들을 알고 있어야하는것처럼 개발자가 되기 위해서는 많은 공부, 복습, 노력이 필요함

728x90
LIST

<< 학습 목표 >>

1. DB와 관련된 모든 자원을 close 해야하는 이유를 설명할 수 있다.

2. DB와 관련된 모든 자원을 close 하는 코드를 작성할 수 있다.


지금까지 우리가 INSERT, UPDATE, DELETE, SELECT 쿼리를 보내고 결과를 받아오는 방법을 봤음

여기서 항상 Connection 을 close 해줬음

 

Connection을 close 해줘야하는 이유는 앞서 설명했듯이 close 를 하지 않으면 디비는 접속한 자바에서 여전히 실행할 쿼리가 남아있다고 판단해 접속을 유지함

close 를 하지 않은 상태에서 다른 자바(DAO)에서 접속을 하려고 한다면 "이미 다른 자바가 접속해있다" 와 비슷한 예외 메세지가 출력됨

그래서 반드시 close 를 해줘야함

close 를 해야하는 이유를 정확히 얘기한다면 완전히 달라지지만 지금 정확하게 얘기한다면 다른 분야의 얘기로 깊게 들어가야하므로 이렇게만 이해해도 충분함

 

우리가 Connection 을 close 하면 내부적으로는 다음과 같은 순서로 close가 됨

1. PreparedStatement 가 있을 경우 PreparedStatment 를 close 함

2. Connection 을 close 함

 

이전 글에서 사용한 select 메서드의 conn.close 메서드의 동작 순서를 통해 시각적으로 close 순서를 보자

우리는 select 메서드의 마지막에 conn.close 메서드를 호출(1) 했지만 close 메서드의 내부에서는 PreparedStatment가 있으니 먼저 PreparedStatment 를 close 함(2)

그 후 Connection을 close 함 (1)

 

 

또한 PreparedStatement 를 close 하면 내부적으로는 다음과 같은 순서로 close가 됨

1. ResultSet이 있을 경우 ResultSet을 close 함

2. PreparedStatement 를 close 함

 

우리는 Connection 을 close했지만(1) Connection의 close 메서드 동작 순서에 의해 PreparedStatment가 close 됨(2)

또한 PreparedStatement가 close 되지만 PreparedStatment의 close 메서드 동작 순서에 의해 먼저 ResultSet이 close 됨(3) 그 후 PreparedStatment가 close 되고(2) 마지막으로 Connection이 close 됨(1) 

 

그림이 다소 복잡해 보이니 간단하게 소스 코드로 보면 아래와 같은 순서로 close 가 됨

 

내부적으로 위와 같이 동작하는 것처럼 DB를 사용하고 나면 DB와 관련된 모든 자원을 반드시 close(해제) 해줘야 DB와의 접속이 완전히 끊김


자바는 어떤 코드가 동작할 때 예외가 발생할 가능성이 있음

close메서드도 마찬가지로 어떤 이유로 예외가 발생해 close 메서드가 호출되지 못할 수 있음

그래서 DAO 에서는 Connection 객체만 close 하면 안되고 직접 ResultSet, PreparedStatement, Connection 순서로 close메서드를 호출해주는게 권장하는 close 방식임(1)

 

그.러.나 이 권장하는 방식에도 문제는 있음

rs.close() 메서드 위에 마우스를 올려둬 ResultSet 객체의 close 메서드 설명을 보자

ResultSet 객체의 close 메서드는 SQLException을 발생시킬 가능성이 있음

ResultSet 객체의 close 메서드를 호출했을 때 SQLException이 발생하는 경우는 무수히 많으니 언제, 왜 발생한다고 명확하게 말할 순 없지만 중요한 점은 ResultSet 객체의 close 메서드가 예외를 발생시킬 가능성이 있다는 것

 

만약 rs.close() 메서드를 호출(1)했는데 예외가 발생했다면?

그 밑에 있는 pstmt.close(), conn.close() 메서드가 호출되지 못하고 catch 로 이동함(2)

그렇다는건? DB와 관련된 자원을 close 하지 못한다는 것

조금 전 위에서 반드시 DB와 관련된 모든 자원은 "반드시" close 를 해줘야한다고 했는데 close가 되지 못하는 상황이 발생함

 

그!래!서! DB와 관련된 모든 자원을 반드시 close 하기 위해서는 일반적으로 다음과 같이 코드를 작성함

public void select() {
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "SELECT * FROM tb";
		
		pstmt = conn.prepareStatement(sql);
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			int nthIdx = rs.getInt(1);
			int nthCol1 = rs.getInt(2);
			String nthCol2 = rs.getString(3);
			Timestamp nthCol3 = rs.getTimestamp(4);
			
			System.out.println("idx => " + nthIdx);
			System.out.println("col1 => " + nthCol1);
			System.out.println("col2 => " + nthCol2);
			System.out.println("col3 => " + nthCol3);
			System.out.println();
		}
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	} finally {
		try {
			if(rs != null) {
				rs.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		try {
			if(pstmt != null) {
				pstmt.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		try {
			if(conn != null) {
				conn.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}

 

DB와 관련된 모든 자원을 try 바깥에 선언하면서 null 상태로 만듬(1)

close 를 try 의 끝에서 하지 않고 finally 에서 함(2)

자바에서 배웠듯 finally 는 예외가 발생 여부에 상관 없이 무조건 동작하는 코드이므로 "반드시" 가 됨

finally 에서 각 객체의 null 여부를 확인하는 이유는 각 객체를 선언한 후 null로 설정을 했기 때문임

오타나 어떤 이유로 인해 class.forName 메서드에서 ClassNotFoundException 예외가 발생한다면(1) catch로 이동(2) 후 finally 가 동작(3)을 하게 되는데 이때 각 객체가 null인 상태이므로 null 여부를 체크하지 않으면 finally 에서 또 다시 NullPointerException이 발생함

일반적으로 DAO의 메서드들은 이와 같은 형태의 코드를 가지니 앞으로 자주보게 될 코드

만약 "왜 코드를 이렇게 써야하지?" 라고 생각이 든다면 지금 단계에서는 가장 마지막에 보여준 finally 까지 포함된 코드를 외우는게 편할 것


DAO 내 select 메서드 코드가 굉장히 길어졌음

그 이유는 쿼리를 보내기 위한 준비(DB 접속) 코드와 접속을 끊는 코드가 덧붙어서 굉장히 길어졌음

또 DAO의 매 메서드 마다 쿼리를 보내기 위한 준비와 접속을 끊는 코드가 매번 추가되야하므로 별도의 메서드로 빼두는게 좋음

 

다음과 같이 Utility 패키지에 DBUtil 클래스를 만들고 아래 코드를 추가하자

package utility;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtil {
	private static final String DRIVER_NAME = "org.mariadb.jdbc.Driver";
	private static final String DB_URL = "jdbc:mariadb://localhost:3306/practice?user=root&password=0000";
	
	public static Connection getConnection() {
		Connection conn = null;
		
		try {
			Class.forName(DRIVER_NAME);
			
			conn = DriverManager.getConnection(DB_URL);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return conn;
	}
	
	private static void close(Connection conn) {
		if(conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
	
	private static void close(PreparedStatement pstmt) {
		try {
			if(pstmt != null) {
				pstmt.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	private static void close(ResultSet rs) {
		try {
			if(rs != null) {
				rs.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	public static void closes(Connection conn, PreparedStatement pstmt) {
		closes(conn, pstmt, null);
	}
	
	public static void closes(Connection conn, PreparedStatement pstmt, ResultSet rs) {
		close(rs);
		close(pstmt);
		close(conn);
	}
}

 

그리고 DAO의 select 메서드를 다음과 같이 바꿔 코드를 줄이자

public void select() {
	Connection conn = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	
	try {
		conn = DBUtil.getConnection();
		
		String sql = "SELECT * FROM tb";
		
		pstmt = conn.prepareStatement(sql);
		rs = pstmt.executeQuery();
		
		while(rs.next()) {
			int nthIdx = rs.getInt(1);
			int nthCol1 = rs.getInt(2);
			String nthCol2 = rs.getString(3);
			Timestamp nthCol3 = rs.getTimestamp(4);
			
			System.out.println("idx => " + nthIdx);
			System.out.println("col1 => " + nthCol1);
			System.out.println("col2 => " + nthCol2);
			System.out.println("col3 => " + nthCol3);
			System.out.println();
		}
	} catch (SQLException e) {
		e.printStackTrace();
	} finally {
		DBUtil.closes(conn, pstmt, rs);
	}
}
728x90
LIST

<< 학습 목표 >>

1. SELECT 쿼리의 결과를 ResultSet 타입 객체에 저장할 수 있다.

2. ResultSet 타입 객체를 통해 결과를 꺼낼 수 있다.


이전 글에서는 INSERT, UPDATE, DELETE 쿼리를 보내고 결과를 받는 방법을 배웠음

이 글에서는 SELECT 쿼리를 보내고 결과를 받는 방법을 배워보자

 

우선 SELECT 쿼리를 위해 데이터를 몇 개 추가하자

INSERT INTO tb(col1, col2, col3) VALUES('1', '첫번째', TIMESTAMP('2023-03-06 14:11:00'));
INSERT INTO tb(col1, col2, col3) VALUES('2', '두번째', TIMESTAMP('2023-03-06 14:12:12'));
INSERT INTO tb(col1, col2, col3) VALUES('4', '네번째', TIMESTAMP('2023-03-06 14:13:26'));

 

chapter04 -> DAO 클래스 내 다음과 같이 select 메서드를 추가하자

public void select() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "SELECT * FROM tb";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		ResultSet rs = pstmt.executeQuery();
		
		while(rs.next()) {
			int nthIdx = rs.getInt(1);
			int nthCol1 = rs.getInt(2);
			String nthCol2 = rs.getString(3);
			Timestamp nthCol3 = rs.getTimestamp(4);
			
			System.out.println("idx => " + nthIdx);
			System.out.println("col1 => " + nthCol1);
			System.out.println("col2 => " + nthCol2);
			System.out.println("col3 => " + nthCol3);
			System.out.println();
		}
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

SELECT 쿼리를 보내 실행한 후 결과를 받아올 때는 executeQuery 메서드를 호출해야함(1)

executeQuery 메서드는 SELECT 쿼리로 조회한 데이터들을 ResultSet 타입 객체에 담아서 반환함(2)

 

executeQuery 메서드가 반환한 ResultSet 타입 객체(SELECT 쿼리의 실행 결과)는 다음과 같이 생겼음
( 조회된 결과 데이터는 제(글쓴이) 상황의 결과임 )

 

ResultSet 타입 객체의 특징은 Cursor(커서) 가 있음

SELECT 결과를 자바에서 사용하려면 ResultSet 타입 객체를 사용해야함

그리고 자바는 Cursor 위치의 데이터를 꺼낼 수 있음

 

현재 커서의 위치는 첫 번째 데이터 보다 한 칸 더 위에 있음

조회 결과에서 첫번째 데이터를 꺼내려면 커서를 한 칸 이동 시킨 후 꺼내야함

 

ResultSet 타입 객체의 커서를 한 칸 이동 시키는 메서드는 next 메서드임

next 메서드는 커서를 이동 시키고 커서가 이동했다면 true를 반환함

만약 커서가 이동하지 못했다면 false를 반환함

 

우리가 사용한 코드에서는 while문의 조건식에 넣었음(1)

 

while문의 동작 과정을 알아보자

우선 executeQuery 메서드를 통해 SELECT 결과가 다음과 같은 형태의 ResultSet 타입 객체에 저장되고 이를 rs 객체에 저장했음

현재 rs 객체의 커서가 첫 번째 데이터 보다 한 칸 위에 있으므로 현재 상태에서는 rs 객체에서 조회 결과 데이터를 꺼낼 수 없음

그래서 next 메서드를 호출해서 커서를 한 칸 뒤로 이동 시킨 후 데이터를 꺼내야함

 

 

while문을 만나면 조건식의 rs.next 메서드가 동작해 커서가 한 칸 이동함

커서가 이동했으므로 rs.next 메서드는 true 를 반환함

 

while의 조건식이 true 이므로 while 안의 코드가 실행됨

rs.getInt(n) 메서드는 "현재 rs 객체의 커서가 가리키는 곳의 n번째 칼럼의 값을 int 로 꺼내라" 임

 

따라서 첫 번째 데이터의 첫 번째 칼럼(idx)의 값이 꺼내짐

 

그 밑에 있는 코드들은 첫 번때 데이터의 두 번째 칼럼(col1), 세 번째 칼럼(col2), 네 번째 칼럼(col3)의 값을 꺼냄

 

그 후 꺼낸 값을 Sysout 함

 

이제 while의 끝을 만나 다시 조건식으로 올라감

조건식의 rs.next 메서드가 동작해 rs 객체의 커서를 다음 칸으로 이동시키고 커서가 이동했으므로 true를 반환함

 

while의 조건식이 true이므로 while 안으로 들어가 현재 커서가 가리키는 두 번째 데이터의 idx, col1, col2, col3 칼럼의 값을 꺼내고(1) 출력함(2)

 

while의 끝을 만나 다시 조건식으로 올라감

조건식의 rs.next 메서드가 동작해 rs 객체의 커서를 다음 칸으로 이동시키고 커서가 이동했으므로 true를 반환함

 

while의 조건식이 true이므로 while 안으로 들어가 현재 커서가 가리키는 두 번째 데이터의 idx, col1, col2, col3 칼럼의 값을 꺼내고(1) 출력함(2)

 

while의 끝을 만나 다시 조건식으로 올라감

조건식의 rs.next 메서드가 동작해 rs 객체의 커서를 다음 칸으로 이동시키고 커서가 이동했으므로 true를 반환함

 

while의 조건식이 true이므로 while 안으로 들어가 현재 커서가 가리키는 두 번째 데이터의 idx, col1, col2, col3 칼럼의 값을 꺼내고(1) 출력함(2)

 

이제 마지막~!

while의 끝을 만나 다시 조건식으로 올라감

조건식의 rs.next 메서드가 동작해 rs 객체의 커서를 다음 칸으로 이동시켜야하는데 커서가 이동할 자리가 없으므로 커서를 옮기지 못하고 false를 반환함


while의 조건식이 false이므로 while을 빠져나가 DB 접속을 끊음


여기까지 SELECT 쿼리를 보내 실행 한 후 결과를 받아오는 방법을 배웠음

또 받아온 결과를 꺼내는 방법까지 배웠음

 

마지막으로 커서에 대해서 조금 더 이야기하고 마무리 하자

커서는 단방향이기 때문에 next 메서드를 호출해 커서를 밑으로 내릴 순 있지만 위로 올릴 수는 없음

아예 위로 올리는 메서드 자체가 없음

 

만약 rs.next 를 해서 커서가 이동을 했는데 다시 첫 번째 데이터를 꺼내야한다면 어쩔 수 없이 executeQuery 메서드를 다시 호출해서 검색 결과를 다시 받아와야함

728x90
LIST

<< 학습 목표 >>

1. INSERT, UPDATE, DELETE 쿼리를 보내 실행하고 결과를 받아올 수 있다.

2. INSERT, UPDATE, DELETE 쿼리의 실행 결과가 무엇인지 알 수 있다.


이전 글에서는 자바와 DB를 연동하는 방법만 배웠음

이 글에서는 INSERT, UPDATE, DELETE 쿼리를 보내고 결과를 받는 방법을 배워보자

 

우선 테이블을 만들자

CREATE TABLE `tb` (
`idx` INT(11) NOT NULL AUTO_INCREMENT,
`col1` INT(11) NULL DEFAULT NULL,
`col2` VARCHAR(50) NULL DEFAULT NULL,
`col3` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`idx`)
)

 

chapter04 -> DAO 클래스를 추가하고 아래 코드를 추가하자

package chapter04;

public class DAO {
	public void insert() {
		
	}
	
	public void update() {
		
	}
	
	public void delete() {
		
	}
}

 

DAO 클래스에 있는 insert, update, delete 메서드를 통해서 INSERT, UPDATE, DELETE 쿼리를 보내고 결과를 받는 방법을 배울 것

 

insert 메서드부터 보자

쿼리를 보내고 결과를 받기 위해서는 항상 먼저 DB에 접속해야함

insert 메서드 안에 DB에 접속하는 코드를 넣어보자

더보기

public void insert() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "전송하고 실행할 쿼리(Query)";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		
		pstmt.executeUpdate();
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

이제 INSERT 쿼리를 작성하려하는데 tb 테이블의 칼럼을 보자

tb 테이블에는 칼럼이 4개가 있지만 idx 칼럼은 기본값이 AUTO_INCREMENT 이므로 col1, col2, col3 칼럼에만 데이터를 저장하면 됨

insert 메서드 내 아래와 같이 INSERT 쿼리를 작성하고 쿼리를 보낸 후 결과를 받아 오는 코드를 넣자

public void insert() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "INSERT INTO tb(col1, col2, col3) VALUES(1, '첫번째', TIMESTAMP('2023-03-06 13:30:00'))";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		int count = pstmt.executeUpdate();
		
		System.out.println("INSERT 쿼리로 영향 받은 행의 수 => " + count);
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

INSERT, UPDATE, DELETE 쿼리를 실행하고 결과를 받아오려면 executeUpdate 메서드를 사용한다 했으므로 executeUpdate 메서드를 사용했음(1)

 

executeUpdate 메서드는 해당 쿼리로 영향 받은 행의 수를 반환함

현재 우리가 1개의 데이터를 INSERT 했으므로 영향 받은 행의 수(INSERT 된 데이터의 수)는 1 임(2)

 

이제 소스 파일을 추가하고 DAO의 insert 메서드를 호출하자

package chapter04;

public class JavaProgram {
	public static void main(String[] args) {
		DAO dao = new DAO();
		dao.insert();
	}
}

 

tb 테이블을 확인해보면 자바에서 보낸 INSERT 쿼리가 실행되 데이터가 저장되어있을 것

 

지금 작성한 INSERT 쿼리는 사실 잘못된 방식으로 INSERT 쿼리를 작성한 것

쿼리를 정확하게 작성해 보내자

방금 작성한 쿼리는 약식으로 작성한 것으로 PreparedStatement 는 쿼리를 작성할 때 "값" 자리는 ? 를 넣어야함

그 후 executeUpdate 메서드를 호출해서 쿼리를 DB로 보내기 전에 ? 자리에 들어갈 값을 채워야함

무슨 말인지 아래 코드를 보자

public void insert() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "INSERT INTO tb(col1, col2, col3) VALUES(?, ?, TIMESTAMP(?))";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1, 2);
		pstmt.setString(2, "두번째");
		pstmt.setString(3, "2023-03-06 13:42:00");
		
		int count = pstmt.executeUpdate();
		
		System.out.println("INSERT 쿼리로 영향 받은 행의 수 => " + count);
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

정확하게 바꾼 쿼리에서 값 자리는 ? 로 채우고(1) executeUpdate 메서드를 호출하기 전 set으로 시작하는 메서드를 통해 ? 자리에 들어갈 값을 채웠음(2)

 

PreparedStatment는 반드시 마지막으로 한, 방금 한 방식으로 쿼리를 구성해 보내야함

마지막으로 한 방식으로 쿼리를 채우지 않고 첫번째로 한 방식으로 쿼리를 구성하면 SQLInjection 이라는 해킹 공격을 받아 DB 내 중요한 개인정보 같은것들이 유출 될 수 있음

 

첫번째로 한 방식(잘못된 방식)과 두 번째로 한 방식(올바른 방식)을 비교해보자

 

<< 첫번째로 한 방식(잘못된 방식) >>

 

<< 두번째로 한 방식(올바른 방식) >>

 

여기까지 INSERT 쿼리 보내고 결과 받기 끝~!


UPDATE, DELETE 는 한번에 같이보자

 

<< update 메서드 >>

public void update() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "UPDATE tb SET col1 = ?, col2 = ?, col3 = TIMESTAMP(?) WHERE idx = ?";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1, 3);
		pstmt.setString(2, "세번째");
		pstmt.setString(3, "2023-03-06 13:54:00");
		pstmt.setInt(4, 1);
		
		int count = pstmt.executeUpdate();
		
		System.out.println("UPDATE 쿼리로 영향 받은 행의 수 => " + count);
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

<< delete 메서드 >>

public void delete() {
	try {
		Class.forName("org.mariadb.jdbc.Driver");
		
		Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
		
		String sql = "DELETE FROM tb WHERE idx = ?";
		
		PreparedStatement pstmt = conn.prepareStatement(sql);
		pstmt.setInt(1, 2);
		
		int count = pstmt.executeUpdate();
		
		System.out.println("DELETE 쿼리로 영향 받은 행의 수 => " + count);
		
		conn.close();
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

 

INSERT, UPDATE, DELETE 방식들을 나란히 놓고 비교해보면 모두 똑같은 방식이라는걸 알 수 있음

 

쿼리 형태를 작성하고(1) 쿼리의 형태를 완성하고(2) 쿼리를 보내 실행 및 결과를 받아오는 방식(3) 임

 

여기까지 자바로 INSERT, UPDATE, DELETE 쿼리를 보내 실행 시키고 결과를 받아오는 방법을 봤음

여기서 중요한 건 SQL을 얼마나 이해하고 있냐임

SQL을 잘 알아야 자바로 내가 원하는 쿼리를 보내 실행시킬 수 있음

728x90
LIST

<< 학습 목표 >>

1. 자바와 DB를 연동해야하는 이유를 설명할 수 있다.

2. DB와 통신(데이터를 주고 받는 행위)하는 자바 클래스를 다른 말로 표현할 수 있다.

3. 자바 클래스와 DB를 연동할 수 있다.


앞서 [ 서블릿 본격적인 시작 ( https://codingaja.tistory.com/21 ) ] 글에서 실제 웹 서비스는 아래와 같이 동작한다고 했음

 

Servlet 카테고리에서 주로 배우는 것들은 컨트롤러(Controller)임

컨트롤러만 있다고 해서 클라이언트의 요청을 완전하게 처리할 수는 없음

 

사용자가 회원 가입을 한다면 회원 정보를 "영구히 저장"해야함

사용자가 로그인을 한다면 "저장되어있는 회원 정보" 중 사용자가 입력한 아이디, 비밀번호와 일치하는 회원 정보를 찾아야함

사용자가 회원 정보 수정을 한다면 "저장되어있는 회원 정보"를 수정해야함

사용자가 회원 탈퇴를 한다면 "저장되어있는 회원 정보"를 삭제해야함

 

이런식으로 사용자가 무언가를 요청했을 때 주로 저장되어있는 정보를 가져와야 요청에 맞는 처리를 할 수 있음

어떤 정보를 안전하게 오랫동안 또는 영구히 저장할 수 있는 방법은 어떤것이 있을까?

 

먼저 파일에 저장하는 방법이 있음

자바의 파일 입출력 방법을 사용해 파일에 정보를 저장한다면 파일을 삭제하기 전까지는 사용자의 정보가 저장되어있음

또한 파일에 저장된 정보를 가져올 수 있음

 

그러나 파일에 저장하면 저장된 정보를 효율적으로 관리(저장, 수정, 삭제, 조회 등)를 할 수가 없음

정보를 효율적으로 관리할 수 있으면서 오랫동안 또는 영구히 저장하는 방법은 DB(Database)를 사용하는 것

DB와 관련된 카테고리는 아니므로 "왜 DB에 정보를 저장하면 효율적으로 관리할 수 있는지", "왜 오랫동안 또는 영구히 저장할 수 있지" 등 DB와 관련된 기초적인 내용은 언급하지 않겠음

만약 DB를 공부해본적이 없다면 여기서 잠깐 멈추고 DB를 끝까지 다 공부하고 오자

DB를 모르면 이후에 있는 Servlet 카테고리의 내용과 JSP 카테고리의 내용을 배울 수 없음


DB는 서블릿에서 연결하는게 아닌 자바(클래스)에서 연결함

 

서블릿은 Controller(컨트롤러 / 제어자)의 역할이라고 했음

그러므로 서블릿은 요청을 받고 파라미터 유효성을 검증함

 

클라이언트의 요청을 처리하는 주체는 서비스(Service / 자바의 클래스)가 담당함

특히 DB와 연결한 후 DB로 쿼리(Query) 를 보내고 쿼리 결과를 받는 역할의 서비스를 DAO(Data Access Object) 라고 부름

 

사용자가 회원 가입 페이지에서 회원 가입 하는 상황을 전체적인 시스템의 흐름으로 알아보자


DAO( Data Access Object / 자바 클래스 ) 가 DB에 접속하기 위해서는 별도의 라이브러리가 필요함

라이브러리는 DB 마다 서로 다름

우리는 MariaDB를 사용하므로 MariaDB가 제공하는 라이브러리를 다운 받아야함

MySQL 또는 Oracle DB를 사용한다면 해당 DB가 제공하는 라이브러리를 다운 받아야함

 

라이브러리를 다운 받기 위해 구글에 maven repository 로 검색하자(1)

그 후 검색 결과의 첫 번째 사이트(2) 로 들어가자

( 만약 검색 결과가 이와 다르다면 직접 이동하자 / https://mvnrepository.com/ )

 

mariadb 로 검색(1) 후 첫 번째 검색 결과(2)로 들어가자

 

어느 버전이든 마음에 드는 버전을 선택해 들어가자

 

jar 형식으로 압축된 라이브러리를 다운 받자(1)

( jar 형식 - 자바에서 인식할 수 있는 압축 형식 )

 

다운 받은 라이브러리를 프로젝트 -> src -> main -> webapp -> WEB-INF -> lib 폴더로 드래그 & 드롭 하자

 

여기까지 DAO가 DB에 접속하기 위해 필요한 라이브러리를 다운 받았음

 

이제 본격적으로 DAO에서 DB에 접속하고(1) 쿼리를 전송하고 실행(2) 한 후 쿼리 실행 결과를 받자보자(3)

 

 

(1), (2), (3) 을 하기 위해 임시로 DAO를 만들자

chapter04 -> DAO 클래스를 추가하고 아래 코드를 추가하자

package chapter04;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DAO {
	public void someMethod() {
		try {
			Class.forName("org.mariadb.jdbc.Driver");
			
			Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");
			
			String sql = "전송하고 실행할 쿼리(Query)";
			
			PreparedStatement pstmt = conn.prepareStatement(sql);
			
//			pstmt.executeUpdate();
			
//			pstmt.executeQuery();
			
			conn.close();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}


[ (1) DB에 접속 ] 하기 위해서는 먼저 lib 폴더에 추가한 라이브러리를 불러와야함(1)

그 후 불러온 라이브러리의 getConnection 메서드를 사용해 DB에 접속함(2)

 

 

DB에 접속하기 위해 getConnection 메서드의 인자로 넣는 값을 자세히 분석해보자

Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/practice?user=root&password=0000");

 

getConnection 메서드의 인자로 넣는 값은 DB에 접속하기 위한 URL임

우리가 네이버에 접속하기 위해 주소창에 https://www.naver.com 을 입력하듯이 DB에 접속하기 위한 URL임

( 만약 "아닌데? 난 네이버에 접속할 때 www.naver.com  으로 입력하는데" 하시는 분은 다시 https://codingaja.tistory.com/16, https://codingaja.tistory.com/17 글을 보고 오자 )

 

jdbc:mariadb:// 는 DB에 접속하기 위해 사용하는 프로토콜임

우리가 네이버에 접속하기 위해 https:// 부터 입력하듯이

만약 MySQL, Oracle DB를 사용해서 해당 DB로 접속해야한다면 프로토콜이 달라짐

 

localhost 는 DB가 동작 중인 서버의 IP 주소임

localhost는 내 컴퓨터의 IP 라는 뜻이라고 했음

 

3306은 DB의 포트번호임

 

practice 는 DB 내 Database명임

( DB와 관련된 기초 내용은 하지 않음 / DB가 Database의 줄임말 아닌가? DB 내 Database명 이라는게 뭐지? 라는 생각이 드는 분은 잠깐 서블릿을 덮고 DB 먼저 공부하고 오자 )

 

? 뒤에 있는것들은 DB에 접속하기 위한 사용자명(아이디)과 비밀번호임

 

이제 위 소스코드를 이어서 보자

이렇게 getConnection 메서드로 접속하면 접속 정보를 담고 있는 Connection 타입 객체가 반환됨

이 객체를 통해서 쿼리를 전송하고 실행할 수 있음

 

코드 가독성을 위해 실행시키고 싶은 쿼리를 먼저 작성(3) 한 후 Connection 객체 안에 들어있는 쿼리를 전송하고 실행시킬 PreparedStatement 객체를 꺼냄(4)

! 여기서 주의 할 점 PreparedStatement 객체를 꺼낼 때 쿼리를 인자로 넣어줘야함

 

여기까지 됐다면 그 다음으로...

실행할 쿼리가 INSERT, UPDATE, DELETE 라면 executeUpdate 메서드를 호출(5-1)하고 실행할 쿼리가 SELECT 라면 executeQuery 메서드를 호출(5-2)해야함

 

이렇게 DB에 쿼리를 전송하기 위해 준비( 1, 2 ) 하고 쿼리를 전송 & 실행 한 후 결과를 받아왔다면 ( 3, 4, 5-1, 5-2) DB와 접속을 끊어야함(6)

DB와 접속을 끊지 않으면 DB는 해당 클래스가 아직 쿼리를 전송 & 실행할 것들이 더 남아있구나 생각해 접속을 계속 유지함

 

이는 마치 음식점에서 음식을 다 먹은 손님이 나가지 않고 계속 그 자리에 앉아있는 상황과 같음

이런 상황이라면 다음 손님이 자리가 나길 계속 기다리면서 음식을 먹지 못하는 것처럼 다른 클래스에서 DB를 사용하기 위해 접속할 수 없는 상태가 발생함


이제 임시로 main을 가지고 있는 자바 소스 파일을 추가하고 DAO 객체를 생성한 후 someMethod 메서드를 호출해보자

( chapter04 -> JavaProgram 소스 파일을 추가하고 아래 코드를 추가하자 )

package chapter04;

public class JavaProgram {
	public static void main(String[] args) {
		DAO dao = new DAO();
		dao.someMethod();
	}
}

 

그 후 해당 소스 파일을 실행시키자

( 소스 파일 내 우클릭 -> [ Run AS ] -> [ Java Application ] )

 

소스 파일을 실행시켰을 때 [ Console ] 패널에 아무것도 뜨지 않았다면 (1), (2), (3) 과정이 제대로 동작했다는 것

만약 [ Console ] 패널에 예외 메세지가 떴다면 someMethod 내 어떤 코드에서 오타가 있거나 DB 내 이름이 practice 인 Database가 없거나 root 사용자 비밀번호가 틀렸을 것, 하나씩 잘 체크하고 다시 실행시켜 보자

( 그래도 예외 메세지가 뜬다면 댓글로 남겨주세요 )


여기까지 자바와 DB를 연동하는 방법을 배웠음

여기서는 자바와 DB를 연동하는 큰 흐름만 배운 것

그 이유는 자바와 DB를 연동하는 과정이 더 많이 남아있음

앞으로 남은 과정들에 설명할 것들이 많고 복잡하기 때문에 이 글에서는 이렇게 큰 흐름만 배우고 다음 글로 넘어가자

728x90
LIST

<< 학습 목표 >>

1. ServletConfig 타입 객체에 대해서 설명할 수 있다.

2. load-on-startup 태그에 대해서 설명할 수 있다.


서블릿으로 개발하다 보면 ServletContext와 ServletConfig 타입 객체를 사용해야하는 경우가 종종 생김

여기서는 ServletConfig 타입 객체가 무엇이고 이 객체들을 통해서 어떤것들을 할 수 있는지 알아보자

 

이전 글에서 보았던 ServletContext 타입 객체의 그림을 다시 보자

 

ServletContext는 서블릿 컨테이너(Servlet Container)가 생성해 관리함

ServletConfig는 서블릿이 생성해 관리함

 

이와 같이 ServletConfig는 서블릿 마다 생성됨

ServletConfig는 서블릿과 관련된 정보를 가지고 있음

서블릿의 이름은 무엇이고 서블릿을 호출하기 위한 경로는 어떻게 되고 등

그리고 ServletConfig 객체를 사용해서 ServletContext 를 꺼낼 수도 있음

 

우리가 직접 ServletConfig를 사용할 일은 없으므로 ServletConfig를 사용해서 ServletContext를 꺼내는 코드를 보고만 넘어가자

package chapter04;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/ServletInfo")
public class ServletInfo extends HttpServlet {
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		ServletConfig servletConfig = getServletConfig();
		
//		ServletContext servletContext = getServletContext();
		ServletContext servletContext = servletConfig.getServletContext();
	}

}

 

먼저 서블릿에서 ServletConfig를 꺼낼 때는 getServletConfig 메서드를 사용함

ServletConfig 타입 객체를 사용해서 ServletContext 를 꺼낼 때는 getServletContext 메서드를 사용함

위 코드에서 주석 처리한 것처럼 ServletContext 를 꺼낼 때 또 다른 방법이 있으니 두 방법 중 상황에 맞는 방법을 사용하면 됨

ServletConfig 타입 객체는 ServletContext 보다 더 사용할 일이 없을 것임


여기서 알아야할 더 중요한 점은 web.xml에 서블릿 관련 설정하는 방법임

web.xml에 적어둔 서블릿 관련 설정 정보가 ServletConfig 타입 객체에 들어감

서블릿으로 개발하면서 ServletConfig 타입 객체를 사용할 일은 거의 없지만

web.xml에 서블릿 관련 설정은 서블릿으로 개발하면서 또는 Spring을 배울 때 사용되므로 알야함

서블릿 관련 설정을 하나 하나 다 이해할 필요는 없고 각 태그만 잘 외우고 있으면 됨

 

사실 web.xml에 서블릿 관련 설정을 이번에 처음 배우는게 아님

앞서 https://codingaja.tistory.com/18 이 글에서 web.xml에 서블릿 관련 설정을 해봤음

 

이제 web.xml에 서블릿 관련 설정하는 방법을 다시 보자

 

web.xml에 이와 같이 servlet, servlet-mapping 태그를 사용해 서블릿 관련 설정을 할 수 있음

servlet 태그는 프로젝트가 서블릿을 인식하게 해주는 태그이고 servlet-mapping 태그는 클라이언트가 서블릿에 접근할 수 있게 해주는 태그임

 

서버가 실행(Start)되면 서블릿도 함께 실행된다고 했음

( 서블릿 실행과 서블릿 호출은 전혀 다름 혹시 ? 라는 생각이 들면 https://codingaja.tistory.com/18 이 글을 읽고 오자 )

서블릿이 실행될 때 실행 순서를 지정해줘야하는 경우가 있음

그때는 servlet 태그 내 load-on-startup 태그(1)를 넣어서 실행 순서를 지정해줌

 

load-on-startup 태그가 없다면 실행 순서를 지정해주지 않았으므로 서버 임의대로 서블릿을 실행시킴

그러나 간혹 서블릿이 순서대로 실행되야하는 경우가 생김

이때 load-on-startup 태그를 사용해 순서를 명시함

 

load-on-startup 태그 내 1부터 기입하면 되고 숫자가 작은 서블릿이 먼저 실행됨

 

load-on-startup 태그는 아무리 늦어도 Spring 에서는 만나게 되니 태그명을 잘 기억해두자

728x90
LIST