스프링 재활훈련 (2)

두번째 시간!
저번에 메이븐으로 프로젝트 만들고 톰캣으로 서버 연결해서 jsp 화면 띄우는걸 성공했으니 이번엔 DB를 연결해서 DB에 있는 정보를 출력하는걸 해보자.
난 포트폴리오 만들때 MySQL을 썼는데 오라클을 자주 쓴다고 한다. 솔직히 UI랑 편의성에서 너무 별로였는데 성능이 좋은걸까.
일단 재활훈련이니 이미 깔려있는 MySQL로 해보고 그 다음에 오라클로 해보자.

시작!

유저 생성

처음부터 해보기 위해서 유저를 만들자. 근데 유저를 만드려면 mysql -u root -p를 치라는데 어디서 치라는지는 안써져있다. 찾아보니 mysql command line client에서 실행하는 것. 그것도 모르고 기본 CMD창에서 허우적거렸다. 좀 자세히 알려주지.
암튼 난 기본적으로 만들어진 root (이거도 처음에 만들라해서 만든것 같다) 외에는 추가한 유저가 없다. select user from user 이 명령어로 사용자를 확인할 수 있다고 해서 쳤는데

No database selected

라는 오류가 뜬다. 단순히 유저 보는데도 데이터베이스를 선택해줘야 하나보다.
>use mysql 명령어를 치면 된다고 하는데 만약 모른다면? 이라는 생각이 든다. 그럴때는 >show databases 를 치면 데이터베이스 목록을 보여준다고 한다. 근데 mysql은 처음 설치하면 기본적으로 깔려있는 데이터베이스 같기도 한데...? 암튼 지식이 늘은 걸로 치자.
위에서 db를 선택헀으면 아까 못했던 select 명령어로 유저를 다시 보자. 아 그리고 명령어 끝에 세미콜론 ( ; ) 붙이는 건 잊지말자. 안붙이면 개행 취급받는지 전 명령어에서 이어진다.
root, mysql.sys, mysql.session, mysql.infoschema 4개가 나온다. root는 아는데 저 mysql 시리즈는 기본 제공되는 사용자인걸까? 그런갑다 하고 넘어가자.
>create user 사용자명 으로 사용자를 추가할 수 있다고 한다. 뒤에 identified by 비밀번호 까지 입력하면 비밀번호를 입력할 수 있다는데 그럼 만약에 사용자를 만들고 뒤에 비밀번호를 추가하려면 어떻게 해야될까?

>alter user 사용자명 identified with mysql_native_password by '비밀번호'

를 치니 됐다.
비밀번호를 확인하기 위해서 >select user, password from user; 를 썼는데 password 컬럼이 없다고 나온다. 찾아보니 authentication_string을 써야된댄다. 8 버전 이후부터인가? 되게 길다. 그래서 쳤는데 글자가 깨진...건 아니고 복호화가 된 상태로 보여주는 것 같다. 암호가 없는 얘는 공백으로 뜨니까.
나 같아도 비밀번호 안보여줄거 같긴한데, 암튼 비밀번호를 잊으면 그냥 교체해야 되는 것 같다.

스키마 생성

사용자를 만들었으니 다음엔 스키마를 만들자.
MySQL Workbench를 켠 다음 커넥션 옆에 있는 +를 눌러서 새 커넥션을 추가해준다. 테스트의 커넥션이니 이름은 test's로 하고 유저명을 내가 방금 만든 유저, tester로 하고 Test Connection을 눌러서 제대로 연결되나 확인해봤다.
암호가 설정되어 있다면 암호를 입력하고 암호가 일치하면 Successfully made the MySQL connection 이라는 알림이 뜬다. 궁금해서 한번 없는 유저나 틀린 비밀번호를 입력해봤는데 Failed to Connect ... 라는 오류가 뜬다.
이렇게 만들면 안에 아무것도 없다. 처음엔 어라 버근가? 했는데 새로 만들어서 아무것도 없는 거였다. SCHEMAS 탭에서 우클릭을 눌러서 Create Schema를 해주자. 그러면

ERROR 1044: Access denied for user 'tester'@'%' to database 'test_schema'

라는 오류가 뜬다. 처음에 유저 생성만 하고 아무 권한도 안줬으니 당연히 권한 오류가 뜰거다. 찾아보니 1044 오류는 권한 오류라고 한다.
그리고 Workbench에서 보여주는 것들은 커넥션, 말 그대로 유저와 데이터베이스 간의 연결이고 해당 유저가 권한을 가진 데이터베이스만 보여주는 것 같다. 난 이 커넥션을 스키마를 만드는 걸로 잘못 알고 있었던 것 같다. 아니 그냥 뭐하는건지 아예 몰랐다.
귀찮긴 하지만 다시 MySQL Command Line으로 넘어가서 스키마를 생성하고 루트에서 tester 유저에 해당 스키마에 대한 권한을 주자.

create database test;

로 test 스키마를 만들고

grant all privileges on test.* to tester;

로 만든 test 스키마에 대한 모든 권한을 tester에게 부여한다. grant에는 all privileges 말고도 select, insert, update 같이 특정 권한만 부여할 수 있다고 한다. 그리고 test가 아니라 test.*인 이유는 test 스키마의 모든 테이블에 대한 권한을 부여해서 그런것 같다. test로 하니 No database selected 에러가 떴었다. 특정 테이블에 대한 권한도 부여할 수 있나본데, 이건 나중에 써보자. 지금 굳이 귀찮게 나눠서 할 일은 없으니.
이렇게 만들고 다시 Workbench로 넘어가서 아까 만든 커넥션에 들어가보면 SCHEMAS에 test 스키마가 딱 보인다.

MySQL 마무리

간단하게 테이블을 만들고 데이터를 넣어보자. 그 전에 사용을 하려면 스키마를 선택해야 된다. 스키마 색이 볼드체로 진해지면 선택되는건데 그냥 스키마를 더블클릭하면 된다.
create 우클릭으로 테이블을 만든다. 명령어로 해도 되는데 귀찮고 복잡하다. 그리고 명령어를 다 잊어서 하나하나 찾아보는데 시간이 너무 많이 걸린다. 테이블명은 number1, 컬럼은 int 값의 id, varchar2(45) 값의 str이다.
그리고 아무값이나 넣으면 MySQL은 끝. 난 완벽한 소수인 17과 test string 이라는 값을 넣었다.

웹 페이지와 연동

이제 남은건 sts로 넘어가서 MySQL과 연동하는 것과 이 테이블에까지 선을 끌어다가 데이터를 뽑아내는 거다.
그러기 위해서는 pom xml에서 의존성을 추가해줘야 한다. jdbc 라고 Java DataBase Connectivity의 약자인데 자바와 DB로 연결해주는 API이다.
일단 교재에 나와있는 의존성은 3개다. spring-jdbc, tomcat-jdbc, mysql-connector-java.
스프링은 알겠고 톰캣은...서버를 연결해주니 필요한건가? MySQL은 오라클도 있으니 따로 전용 클래스가 필요한 것이겠고. 뭐 암튼 넣어주고 업데이트를 돌려주자.

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.apache.tomcat</groupId>
	<artifactId>tomcat-jdbc</artifactId>
	<version>8.5.27</version>
</dependency>
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.45</version>
</dependency>

그리고 DB를 연결하기 위한 설정 클래스도 만들어준다. 이런건 config 패키지에서 일괄 관리하게 하자. 이걸 xml로 설정할수도 있다고 했던것 같은데 내가 xml 혐오자라 무시해서 어떻게 했는지 기억이 안난다.

public class DbConfig {

	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/test?characterEncoding=utf8");
		ds.setUsername("tester");
		ds.setPassword("1234");
		ds.setInitialSize(2);
		ds.setMaxActive(10);
		ds.setTestWhileIdle(true);
		ds.setMinEvictableIdleTimeMillis(60000 * 3);
		ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
		return ds;
	}
}

그 뒤에는 매퍼를 넣어주자. 매퍼는 데이터베이스와 자바를 가장 자세하게 연동시켜주는 파일로 알고 있다. xml 파일을 만들어 준 다음

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

을 넣어줘서 매퍼 형식을 갖추게 해준다. 이러면 자동완성이 딱 되서 편하다.

매퍼는 MyBatis를 쓸 때 사용하는 거다. 편하긴한데 일단 지금은 이런 API 없이 기본부터 하자.
Dao 클래스를 만들어준다. connection과 statement, resultset 등을 임포트하고 위에서 만든 데이터소스를 autowired로 넣어준다. 그리고 위에서 쓴 것들을 활용해서 데이터를 꺼내는 메소드를 만들어야되는데, Vo를 안만드려했는데 안만드니 여간 귀찮은게 아니다. 그리고 Vo가 아닌 맵 방식으로 해본적이 거의 없어서 기억도 안나니 지금은 그냥 DB용 Vo를 만들어주자.

public class Vo {
	private int id;
	private String str;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getStr() {
		return str;
	}
	public void setStr(String str) {
		this.str = str;
	}
}

Vo는 대충 이렇다. lombok을 쓰면 변수들만 쓰고 Getter, Setter를 안써도 되는데 그건 나중에 하자. 일단 jsp에 띄우는게 성공하면 Vo 지우고 Map 방식으로 해볼거다.

public class Dao {
	private Connection conn;
	private PreparedStatement pstmt;
	private ResultSet rs;
	
	@Autowired
	private DataSource dbConfig;
	
	public ArrayList<Vo> getDataAll() {
		ArrayList<Vo> list = new ArrayList<>();
		try {
			System.out.println("getData 실행");
			String sql = "select * from number1";
			conn = dbConfig.getConnection();
			pstmt = conn.prepareStatement(sql);
			rs = pstmt.executeQuery();
			while(rs.next()) {
				Vo vo = new Vo();
				vo.setId(rs.getInt("id"));
				vo.setStr(rs.getString("str"));
				System.out.println(vo.getStr());
				list.add(vo);
			}
		} catch (Exception e) {
			System.out.println(e);
		} finally {
			try {
				if(rs != null) {
					rs.close();
				}
				if(pstmt != null) {
					pstmt.close();
				}
				if(conn != null) {
					conn.close();
				}
			} catch(Exception e) {
				System.out.println(e);
			}
		}
		return list;
	}
}

그리고 Dao는 이렇게 됐다. 이게 Dao인지 Service인지 헷갈리는데 일단 해보자. 근데 MyBatis를 안쓰니 이렇게 복잡해지는구나 싶다. 그리고 위에서 Statement 대신 PreparedStatement를 사용했는데 Statement는 인젝션이라고 해커가 1==1) 값을 넣으면 무조건 참이 되어서 문제가 터지는데 PreparedStatement는 그걸 막을 수 있다고 해서 사용한거다. 솔직히 직접 안봐서 어떻게 되는건진 모르겠는데 그냥 쓰래서 쓴거다.
근데 실행을 하면 클래스를 찾을 수 없다니 설정 위치가 잘못됐다니 하면서 오류가 터졌는데 어찌저찌 고쳐가면서 완성했다. 일단 서버까지는 제대로 작동한다. 근데 DB 페이지로 들어가려하면

javax.el.PropertyNotFoundException: 타입 [java.lang.String]에서 프로퍼티 [id]을(를) 찾을 수 없습니다.

라는 오류가 뜬다. 뭔가 잘못된거 같다. 차근차근 해결해봐야겠다.
첫번째로 아예 DB랑 연결이 안되는건지 보기위해서 sts 콘솔창에서 System.out으로 출력시켜봤다. 근데 여기서는 제대로 뜬다? 뭐지???
그렇다면 Java에서는 제대로 불러왔는데 jsp로 보내는 과정에서 오류가 터지는것 같다. 교재를 다시 보니 난 JDBC 템플릿이라는 클래스를 안썼다. 근데 JDBC면 아예 콘솔에서도 DB가 안떠야되는거 아닌가?

일단 내 컨트롤러는 이렇고

@Controller
public class MainController {
	@Autowired
	private Dao dao;
	
	@RequestMapping("/main")
	public String mainPage() {
		System.out.println("컨트롤러 실행");
		return "index.jsp";
	}
	@RequestMapping("/db")
	public String dbPage(Model model) {
		System.out.println("db 컨트롤러 실행");
		model.addAttribute("columns", dao.getDataAll());
		return "dbTest.jsp";
	}
}

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>Insert title here</title>
</head>
<body>
<p>db 페이지</p>
<c:forEach var="column" items="columns">
	<p>${column.id} is ${column.str}</p>
</c:forEach>
</body>
</html>

이게 뭔일인가 싶어서 컨트롤러랑 jsp를 바꿔봤다. jsp 태그 사용법이 잘못됐나 해서.
dbPage 메서드에서 dao 사용부분을 없애고 Map을 임시로 만들어서 값을 출력하는 방식.

HashMap<String, String> map = new HashMap<String, String>();
map.put("id", "1");
map.put("str", "test str");
model.addAttribute("columns", map);

이렇게하고 jsp는 반복문 없이 ${columns.id}를 출력했더니 정상적으로 나온다.
jsp 태그 문제인거같다. 찾아보니 items가 java의 model을 받아오는게 아니고 그냥 문자값이다. 달러로 바꿔준다. 이랬는데 이번엔 글자만 바꿔서 또 오류가 터진다. 생각해보니 foreach로 했는데. 모델에서 넣어준건 map이다. ArrayList 안에 넣어주자. 그러니 해결이 됐다. 인간승리!
결국 안된 이유는 columns가 ${columns}가 아니기 때문이였다. 글자 하나 차이로 이렇게 개고생을 한다.
원인을 찾았으니 깔끔하게 정리해보자.

정리

DB 설정 클래스를 만들어준다. DataSource 클래스를 임포트하고 사용할 드라이버 클래스, 데이터베이스 URI, 유저명, 비밀번호, 커넥션 크기, 유지시간 등등 (이런건 솔직히 잘 안써봐서 모르겠다.) 을 지정해주고 Bean으로 등록해준다.
그 뒤에 DB에 연결해주는 DAO (MyBatis에서는 DAO 클래스와 Mapper xml 등을 활용해서 딱딱 구분지었었는데 지금은 Dao에 쿼리까지 들어있는 원시적인 형태여서 솔직히 이걸 뭐라 불러야 되는지는 모르겠다.) 를 만들어준다.
Connection, PreparedStatement, ResultSet을 임포트하고 아까 만든 DB 설정 클래스에서 DataSource를 Autowired로 가져온다.
그리고는 try로 sql문을 넣어주고 DataSource를 통해서 Connection의 getConnection 메서드를 써서 연결.
Connection의 preparedStatement(SQL문) 메서드를 사용해서 PreparedStatement 정의.
PreparedStatement의 executeQuery() 를 사용해서 ResultSet 을 돌려주고 원하는 방식으로 데이터를 넣는다. 반환형이랑 넣는 방식은 케바케다.
그리고 finally 에서 ResultSet, PrepareStatement, Connection을 close() 로 모두 종료해주고 데이터를 반환해준다.
성공했으니 Map으로 한번 받아오려했는데 생각했던 것보다 너무 간단했다.

ArrayList<Map<String, String>> list = new ArrayList<>();
try {
	String sql = "select * from number1";
	conn = dbConfig.getConnection();
	pstmt = conn.prepareStatement(sql);
	rs = pstmt.executeQuery();
	while(rs.next()) {
		Map<String, String> map = new HashMap<String, String>();
		map.put("id", rs.getString("id"));
		map.put("str", rs.getString("str"));
		list.add(map);
	}
}

그냥 Vo 클래스를 Map으로 바꾼거다. 게다가 id 컬럼은 int형이라서 ResultSet의 getString() 으로 정수형이 불러와지나? 싶었는데 된다.
이게 맞나? 싶기도 한데 뭐 되니까 넘어가자. MyBatis 쓸때 ResultMap으로 써보면 알겠지.

후기

복잡하다. 데이터 뽑기만 한건데 뭐이리 복잡해진건지.
그런데 contextConfigLocation 이라는 파라미터의 기능을 모르겠다. 내 생각에는 저 Dao를 실행할때 @Configuration이라는 어노테이션이 있어야 Bean 등록이 되는걸로 알고있는데 없어도 제대로 돌아가니 뭐가 뭔지 모르겠다. 구동방식에 대한 이해가 있어야겠다.
이번엔 데이터를 한번 넣어보자.
자신감도 생겼겠다 한번 막 만들어봤는데 오류가 터졌다. 쿼리문 문제인것 같아서 로그를 찍으려했는데 로그가 안나온다. logback xml 파일을 지워보니 기본 로그 같은게 나온다. 근데 logback 파일을 남긴채로 전부 주석처리를 하니 로그가 하나도 안찍힌다.
쉣. 떡상했다가 떡락하는 코인마냥 기분이 오락가락한다. 그래서 로그는 포기하고 넣는거라도 해보려고 찾고 있는데 책에서는 내가 한 방식대로 안하고 있다. JDBCTemplate? 를 쓰고 있는거다. 나처럼 원시인마냥 커넥션 열고 닫고를 하지 않는다.
그제서야

아 이거 스프링이 아니고 JSP 웹 프로그래밍이구나

라는 깨달음을 얻었다. 미친.
결과가 어찌됐건 돌고돌아 처음으로 다시왔다. 일단 지금은 서블릿 방식으로 하고 차츰 단계를 올려가자. 처음부터 공부했어야 됐으니 잘된거다.
그리고 오류가 터지는 이유는 한번 sql문을 DB에서 실행시켰더니 문법 오류란다. values를 안썼다. 죽이고 싶은 오타.
그리고 PreparedStatement 사용에서 오류가 있어서 조금 수정했다. sql문을 넣고 인수 없이 사용해야되는데 인수를 또 넣어버려서 쿼리가 실행되지 않았다. 실수라 하면 실순데 쿼리문이 로그로 안찍히니 죽을맛이다. 쿼리문만 보여줘도 바로 찾는건데.
InsertDao의 코드는 결과적으로 이렇다.

public void insertData(Map<String, String> map) throws SQLException {
	try {
		String sql = "insert into number1 values (?, ?)";
		conn = dbConfig.getConnection();
		conn.setAutoCommit(false);
		pstmt = conn.prepareStatement(sql);
		pstmt.setString(1, map.get("id"));
		pstmt.setString(2, map.get("str"));
		int result = pstmt.executeUpdate();
		System.out.println(result);
		if(result==0) {
			throw new SQLException();				
		} else {
			conn.commit();
		}
	} catch (SQLException e) {
		conn.rollback();
	} finally {
		if(pstmt != null) {
			pstmt.close();
		}
		if(conn != null) {
			conn.close();
		}
	}
}

그리고 의문점이 또 한가지. DB상에서 int와 VARCHAR 형은 분명히 다른 타입이고 문자열을 입력하려면 ''을 넣어야 할거다.
근데 Map 방식으로 데이터를 넣을때 Map<String, String>으로 했다가 id 컬럼이 int 형이여서 Object로 바꿨었다. 근데 더 복잡해져서 에라 모르겠다 하고 String으로 다시 되돌렸는데 오류가 안떴다. 이게 뭔일인가? pstmt의 setString은 ? 부분을 문자열로 치환하는거 아닌가? 하지만 로그가 안찍히니 볼 수도 없다. 개가튼거.

암튼 다음에 할 것은 세가지다.

1. 로그를 어떻게 찍는가.
2. JDBCTemplate 을 써서 DB 연동하기.
3. 서블릿 작동방식 확인하기.

좋은 웹페이지 즐겨찾기