개발자입니다
[비트캠프] 70일차(15주차2일) - JDBC: myapp-39~42중간(커넥션 풀, PreparedStatement, Mybatis) 본문
[비트캠프] 70일차(15주차2일) - JDBC: myapp-39~42중간(커넥션 풀, PreparedStatement, Mybatis)
끈기JK 2023. 2. 14. 13:16
39. 멀티스레드와 DB 커넥션 툴
Local 의 C1, C2, C3 에서 Application Server 접속시 Thread 생성하여 Handler 와 대응한다. Handler 에서 DAO 로 Connection 객체로 통신하고 여기서 DBMS Table 의 Thread 와 통신한다. Thread 는 Table 과 I/O 한다.
① 이전 방식
C1 이 insert, update 한다. 그러면 DBMS Thread 의 임시DB 에 insert, update 한다.
C3 가 delete, delete 한다. 그러면 DBMS Thread 의 임시DB 에 delete, delete 한다.
C2 가 update, delete 한다. 그러다 실행 중 오류 발생하고 rollback 요청한다.
C2 가 rollback 을 요청하면 Thread가 보관한 임시 작업 결과를 모두 취소한다! 문제는 C1 과 C3 의 작업 결과도 취소된다는 것이다!
왜? DB 커넥션을 공유하기 때문이다.
☆결론
멀티 스레드 환경에서 DB Connection 을 공유하지 말라!
멀티 스레드가 DB 커넥션을 공유할 때 발생하는 문제점 경험하기
C1 에서 학생 입력한다. Application Server 에 Thread 생성되고 member insert, student insert 한다. DBMS Thread 의 임시DB 에 명령이 입력된다. 그 후 30초간 정지된다.
C2 에서 강사 입력한다. Applicatoin Server 에 Thread 생성되고 member insert, teacher insert 하는데 DBMS Thread 의 임시 DB 에 명령이 입력된다. 그러다 오류 발생해서 rollback 된다.
C1 에서 30초가 지나 commit 한다. 그러나 C2 에서 rollback 했기 때문에 commit 된 것은 없다. → 커넥션을 공유하기 때문
Thread 별로 Connection 관리
ConnectionFactory 에 ThreadLocal<Connection> conLocal 선언한다. getConnection() 메서드 생성한다.
DAO 에서 ConnectionFactory 객체 포함한다. insert(), update(), ... 으로 getConnection() ① call 하면 con 객체 ② return 한다. 이를 ③ 사용 한다.
스레드 별로 Connection 관리
C1 에서 학생 입력한다. Application Server 에 Thread 생성되고 여기서 ConnectionFactory 에게 getConnection() 으로 Con 요청해서 return 받는다. insert, insert 하면 DBMS Thread 의 임시DB 에 insert, insert 명령 결과가 저장된다.
C2 에서 강사 입력한다. Application Server 에 Thread 생성되고 여기서 ConnectionFactory 에게 getConnection() 으로 Con 요청해서 return 받는다. insert, insert 중 오류 발생하면 DBMS Thread 의 임시DB insert 된 것 rollback 으로 취소한다.
C1, C2 의 Con 다르기 때문에 C2 의 rollback 이 C1 에 영향 끼치지 않는다.
39. 멀티 스레드와 DB 커넥션 관리
- Thread를 이용하여 다중 클라이언트 요청을 동시에 처리하는 방법
- 멀티 스레드 환경에서 DB 커넥션을 다루는 방법
ConnectionFactory.java 파일 생성해서 util 패키지에 넣는다.
현재 스레드의 ThreadLocal 에 con 객체 있으면 리턴하고 없으면 생성해서 리턴한다.
public class ConnectionFactory {
String jdbcUrl;
String username;
String password;
// 스레드 전용 변수
// => 스레드 마다 별도로 존재하는 변수다.
// => Connection 객체를 담는 변수다.
ThreadLocal<Connection> conLocal = new ThreadLocal<>();
public ConnectionFactory(String jdbcUrl, String username, String password) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
}
public Connection getConnection() throws Exception {
// 현재 이 명령을 실행하는 스레드에 마련된 conLocal 변수에서 값을 꺼낸다.
Connection con = conLocal.get();
if (con == null) {
// 현재 스레드 변수에 값이 Connection 객체가 들어 있지 않다면 새로 만든다.
con = DriverManager.getConnection(jdbcUrl, username, password);
// 현재 스레드가 다음에도 사용할 수 있도록 Connection 객체를 스레드 변수에 보관한다.
conLocal.set(con);
System.out.printf("[%s] Connection 객체 생성!\n", Thread.currentThread().getName());
}
System.out.printf("[%s] Connection 객체 리턴!\n", Thread.currentThread().getName());
// 현재 스레드를 위해 준비한 Connection 객체를 리턴한다.
return con;
}
}
ConnectionFactory 객체 생성한다. ----DaoImpl, ----Handler 에 주입한다.
execute() 에서 serverSocket().accept() 이 후 Thread 생성해서 다중 사용자와 대응한다.
public class ServerApp {
ConnectionFactory conFactory = new ConnectionFactory(
"jdbc:mariadb://localhost:3306/studydb", "study", "1111");
StudentHandler studentHandler;
TeacherHandler teacherHandler;
BoardHandler boardHandler;
HelloHandler helloHandler = new HelloHandler();
public static void main(String[] args) {
try {
new ServerApp().execute(8888);
} catch (Exception e) {
System.out.println("서버 실행 오류!");
e.printStackTrace();
}
}
public ServerApp() throws Exception{
BoardDaoImpl boardDao = new BoardDaoImpl(conFactory);
MemberDaoImpl memberDao = new MemberDaoImpl(conFactory);
StudentDaoImpl studentDao = new StudentDaoImpl(conFactory);
TeacherDaoImpl teacherDao = new TeacherDaoImpl(conFactory);
this.studentHandler = new StudentHandler("학생", conFactory, memberDao, studentDao);
this.teacherHandler = new TeacherHandler("강사", conFactory, memberDao, teacherDao);
this.boardHandler = new BoardHandler("게시판", boardDao);
}
void execute(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중...");
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> service(socket)).start();
}
} catch (Exception e) {
System.out.println("서버 소켓 오류!");
e.printStackTrace();
}
}
private void hello(StreamTool streamTool) throws Exception {
streamTool.println("비트캠프 관리 시스템")
.println(" Copyright by 네이버클라우드1기")
.println("--------------------------------------")
.println("안녕하세요!")
.println()
.send();
}
private void processRequest(StreamTool streamTool) throws Exception {
loop: while (true) {
String command = streamTool.readString();
if (command.equals("menu")) {
menu(streamTool);
continue;
}
int menuNo;
try {
menuNo = Integer.parseInt(command);
} catch (Exception e) {
streamTool.println("메뉴 번호가 옳지 않습니다!").println().send();
continue;
}
try {
switch (menuNo) {
case 1:
studentHandler.service(streamTool);
break;
case 2:
teacherHandler.service(streamTool);
break;
case 3:
boardHandler.service(streamTool);
break;
case 4:
helloHandler.service(streamTool);
break;
case 9:
break loop; // loop 라벨이 붙은 while 문을 나간다.
default:
streamTool.println("잘못된 메뉴 번호 입니다.").send();
}
} catch (Exception e) {
streamTool.printf("명령 실행 중 오류 발생! - %s : %s\n",
e.getMessage(),
e.getClass().getSimpleName()).send();
}
}
// while 종료하면 클라이언트와 연결을 끊는다.
streamTool.print("quit").send();
}
void menu(StreamTool streamTool) throws Exception {
streamTool.println("1. 학생관리")
.println("2. 강사관리")
.println("3. 게시판")
.println("4. 인사")
.println("9. 종료")
.println("메뉴 번호:")
.send();
}
public void service(Socket clientSocket) {
// 스레드가 실행할 코드를 둔다.
try (Socket socket = clientSocket;
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
// 입출력 보조 도구 준비
StreamTool streamTool = new StreamTool(in, out);
String clientIP = socket.getInetAddress().getHostAddress();
System.out.printf("접속: %s\n", clientIP);
hello(streamTool);
processRequest(streamTool);
System.out.printf("끊기: %s\n", clientIP);
} catch (Exception e) {
System.out.println("클라이언트 요청 처리 오류!");
e.printStackTrace();
}
}
}
----DaoImpl 생성자에 ConnectionFactory 객체 주입한다.
conFactory.getConnection() 으로 con 객체 가져온다.
public class BoardDaoImpl implements BoardDao {
ConnectionFactory conFactory;
public BoardDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Board b) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("insert into app_board(title, content, pwd) values('%s', '%s', '%s')",
b.getTitle(), b.getContent(), b.getPassword());
stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Board> findAll() {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"select board_id, title, created_date, view_cnt from app_board order by board_id desc")) {
ArrayList<Board> list = new ArrayList<>();
while (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
list.add(b);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Board findByNo(int no) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"select board_id, title, content, pwd, created_date, view_cnt from app_board where board_id=" + no)) {
if (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setContent(rs.getString("content"));
b.setPassword(rs.getString("pwd"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
return b;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public void increaseViewCount(int no) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format(
"update app_board set"
+ " view_cnt = view_cnt + 1"
+ " where board_id=%d",
no);
stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Board> findByKeyword(String keyword) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"select board_id, title, created_date, view_cnt"
+ " from app_board"
+ " where title like('%" + keyword + "%')"
+ " or content like('%" + keyword + "%')"
+ " order by board_id desc")) {
ArrayList<Board> list = new ArrayList<>();
while (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
list.add(b);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Board b) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("update app_board set title='%s', content='%s' where board_id=%d",
b.getTitle(), b.getContent(), b.getNo());
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("delete from app_board where board_id=%d", no);
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class MemberDaoImpl implements MemberDao {
ConnectionFactory conFactory;
public MemberDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Member m) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format(
"insert into app_member(name, email, pwd, tel)"
+ " values('%s', '%s', sha2('%s',256), '%s')",
m.getName(),
m.getEmail(),
m.getPassword(),
m.getTel());
stmt.executeUpdate(
sql,
Statement.RETURN_GENERATED_KEYS // insert 실행 후 자동 증가된 PK 값을 리턴 받겠다고 선언한다.
);
// 1) insert 후에 자동 생성된 PK 값을 꺼내올 도구를 준비한다.
try (ResultSet keyRs = stmt.getGeneratedKeys()) {
// 2) keyRs 도구를 이용하여 서버에서 PK 값을 가져온다.
keyRs.next();
// 3) 가져온 PK 레코드에서 PK 값을 꺼낸다.
int autoGeneratedMemberId = keyRs.getInt(1); // PK 컬럼에 한 개이기 때문에 PK 레코드에도 한 개의 컬럼만 있다.
// 4) DBMS에서 insert 할 때 자동 생성한 member_id 값을 Member 객체에 리턴한다.
m.setNo(autoGeneratedMemberId);
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Member> findAll() {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"select member_id, name, email, created_date"
+ " from app_member"
+ " order by member_id desc")) {
ArrayList<Member> list = new ArrayList<>();
while (rs.next()) {
Member m = new Member();
m.setNo(rs.getInt("member_id"));
m.setName(rs.getString("name"));
m.setEmail(rs.getString("email"));
m.setCreatedDate(rs.getDate("created_date"));
list.add(m);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Member findByNo(int no) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"select member_id, name, email, tel, created_date"
+ " from app_member"
+ " where member_id=" + no)) {
if (rs.next()) {
Member m = new Member();
m.setNo(rs.getInt("member_id"));
m.setName(rs.getString("name"));
m.setEmail(rs.getString("email"));
m.setTel(rs.getString("tel"));
m.setCreatedDate(rs.getDate("created_date"));
return m;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Member m) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format(
"update app_member set "
+ " name='%s', email='%s', pwd=sha2('%s',256), tel='%s'"
+ " where member_id=%d",
m.getName(),
m.getEmail(),
m.getPassword(),
m.getTel(),
m.getNo());
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format(
"delete from app_member"
+ " where member_id=%d", no);
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class StudentDaoImpl implements StudentDao {
ConnectionFactory conFactory;
public StudentDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Student s) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("insert into app_student("
+ " member_id,"
+ " pst_no,"
+ " bas_addr,"
+ " det_addr,"
+ " work,"
+ " gender,"
+ " level)"
+ " values('%s','%s','%s','%s',%b,'%s',%d)",
s.getNo(), // app_member 테이블에 입력한 후 자동 생성된 PK 값
s.getPostNo(),
s.getBasicAddress(),
s.getDetailAddress(),
s.isWorking(),
s.getGender(),
s.getLevel());
stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Student> findAll() {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery("select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " s.work,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " order by"
+ " m.name asc")) {
ArrayList<Student> list = new ArrayList<>();
while (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setWorking(rs.getBoolean("work"));
s.setLevel(rs.getByte("level"));
list.add(s);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Student findByNo(int no) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery("select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " m.created_date,"
+ " s.pst_no,"
+ " s.bas_addr,"
+ " s.det_addr,"
+ " s.work,"
+ " s.gender,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " where s.member_id=" + no)) {
if (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setCreatedDate(rs.getDate("created_date"));
s.setPostNo(rs.getString("pst_no"));
s.setBasicAddress(rs.getString("bas_addr"));
s.setDetailAddress(rs.getString("det_addr"));
s.setWorking(rs.getBoolean("work"));
s.setGender(rs.getString("gender").charAt(0));
s.setLevel(rs.getByte("level"));
return s;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Student> findByKeyword(String keyword) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery("select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " s.work,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " where"
+ " m.name like('%" + keyword + "%')"
+ " or m.email like('%" + keyword + "%')"
+ " or m.tel like('%" + keyword + "%')"
+ " or s.bas_addr like('%" + keyword + "%')"
+ " or s.det_addr like('%" + keyword + "%')"
+ " order by"
+ " m.member_id desc")) {
ArrayList<Student> list = new ArrayList<>();
while (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setWorking(rs.getBoolean("work"));
s.setLevel(rs.getByte("level"));
list.add(s);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Student s) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("update app_student set "
+ " pst_no='%s',"
+ " bas_addr='%s',"
+ " det_addr='%s',"
+ " work=%b,"
+ " gender='%s',"
+ " level=%d "
+ " where member_id=%d",
s.getPostNo(),
s.getBasicAddress(),
s.getDetailAddress(),
s.isWorking(),
s.getGender(),
s.getLevel(),
s.getNo());
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("delete from app_student"
+ " where member_id=%d", no);
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class TeacherDaoImpl implements TeacherDao {
ConnectionFactory conFactory;
public TeacherDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Teacher s) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("insert into app_teacher("
+ " member_id,"
+ " degree,"
+ " school,"
+ " major,"
+ " wage)"
+ " values(%d, %d,'%s','%s',%d)",
s.getNo(),
s.getDegree(),
s.getSchool(),
s.getMajor(),
s.getWage());
stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Teacher> findAll() {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery("select"
+ " m.member_id,"
+ " m.name,"
+ " m.tel,"
+ " t.degree,"
+ " t.major,"
+ " t.wage"
+ " from app_teacher t"
+ " inner join app_member m on t.member_id = m.member_id"
+ " order by"
+ " m.name asc")) {
ArrayList<Teacher> list = new ArrayList<>();
while (rs.next()) {
Teacher s = new Teacher();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setTel(rs.getString("tel"));
s.setDegree(rs.getInt("degree"));
s.setMajor(rs.getString("major"));
s.setWage(rs.getInt("wage"));
list.add(s);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Teacher findByNo(int no) {
try (Statement stmt = conFactory.getConnection().createStatement();
ResultSet rs = stmt.executeQuery("select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " m.created_date,"
+ " t.degree,"
+ " t.school,"
+ " t.major,"
+ " t.wage"
+ " from app_teacher t"
+ " inner join app_member m on t.member_id = m.member_id"
+ " where m.member_id=" + no)) {
if (rs.next()) {
Teacher s = new Teacher();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setCreatedDate(rs.getDate("created_date"));
s.setDegree(rs.getInt("degree"));
s.setSchool(rs.getString("school"));
s.setMajor(rs.getString("major"));
s.setWage(rs.getInt("wage"));
return s;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Teacher t) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format(
"update app_teacher set "
+ " degree=%d,"
+ " school='%s',"
+ " major='%s',"
+ " wage=%d "
+ " where member_id=%d",
t.getDegree(),
t.getSchool(),
t.getMajor(),
t.getWage(),
t.getNo());
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (Statement stmt = conFactory.getConnection().createStatement()) {
String sql = String.format("delete from app_teacher"
+ " where member_id=%d", no);
return stmt.executeUpdate(sql);
} catch (Exception e) {
throw new DaoException(e);
}
}
}
----Handler에서 input, modify, deleteMember 할때 현재 스레드의 Connection 객체를 리턴 받는다.
public class StudentHandler {
private ConnectionFactory conFactory;
private MemberDao memberDao;
private StudentDao studentDao;
private String title;
public StudentHandler(String title, ConnectionFactory conFactory, MemberDao memberDao, StudentDao studentDao) {
this.title = title;
this.conFactory = conFactory;
this.memberDao = memberDao;
this.studentDao = studentDao;
}
private void inputMember(StreamTool streamTool) throws Exception {
Student s = new Student();
s.setName(streamTool.promptString("이름? "));
s.setEmail(streamTool.promptString("이메일? "));
s.setPassword(streamTool.promptString("암호? "));
s.setTel(streamTool.promptString("전화? "));
s.setPostNo(streamTool.promptString("우편번호? "));
s.setBasicAddress(streamTool.promptString("주소1? "));
s.setDetailAddress(streamTool.promptString("주소2? "));
s.setWorking(streamTool.promptInt("0. 미취업\n1. 재직중\n재직자? ") == 1);
s.setGender(streamTool.promptInt("0. 남자\n1. 여자\n성별? ") == 0 ? 'M' : 'W');
s.setLevel((byte) streamTool.promptInt("0. 비전공자\n1. 준전공자\n2. 전공자\n전공? "));
// 현재 스레드가 갖고 있는 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
memberDao.insert(s);
studentDao.insert(s);
con.commit();
streamTool.println("입력했습니다!").send();
} catch (Exception e) {
con.rollback();
streamTool.println("입력 실패입니다!").send();
e.printStackTrace();
} finally {
con.setAutoCommit(true);
}
}
private void printMembers(StreamTool streamTool) throws Exception {
List<Student> members = this.studentDao.findAll();
streamTool.println("번호\t이름\t전화\t재직\t전공");
for (Student m : members) {
streamTool.printf("%d\t%s\t%s\t%s\t%s\n",
m.getNo(), m.getName(), m.getTel(),
m.isWorking() ? "예" : "아니오",
getLevelText(m.getLevel()));
}
streamTool.send();
}
private void printMember(StreamTool streamTool) throws Exception {
int memberNo = streamTool.promptInt("회원번호? ");
Student m = this.studentDao.findByNo(memberNo);
if (m == null) {
streamTool.println("해당 번호의 학생이 없습니다.").send();
return;
}
streamTool
.printf(" 이름: %s\n", m.getName())
.printf(" 전화: %s\n", m.getTel())
.printf("우편번호: %s\n", m.getPostNo())
.printf("기본주소: %s\n", m.getBasicAddress())
.printf("상세주소: %s\n", m.getDetailAddress())
.printf("재직여부: %s\n", m.isWorking() ? "예" : "아니오")
.printf(" 성별: %s\n", m.getGender() == 'M' ? "남자" : "여자")
.printf(" 전공: %s\n", getLevelText(m.getLevel()))
.printf(" 등록일: %s\n", m.getCreatedDate())
.send();
}
// 인스턴스 멤버(필드나 메서드)를 사용하지 않기 때문에
// 그냥 스태틱 메서드로 두어라!
private static String getLevelText(int level) {
switch (level) {
case 0: return "비전공자";
case 1: return "준전공자";
default: return "전공자";
}
}
private void modifyMember(StreamTool streamTool) throws Exception {
int memberNo = streamTool.promptInt("회원번호? ");
Student old = this.studentDao.findByNo(memberNo);
if (old == null) {
streamTool.println("해당 번호의 회원이 없습니다.").send();
return;
}
// 변경할 데이터를 저장할 인스턴스 준비
Student m = new Student();
m.setNo(old.getNo());
m.setCreatedDate(old.getCreatedDate());
m.setName(streamTool.promptString(String.format("이름(%s)? ", old.getName())));
m.setEmail(streamTool.promptString(String.format("이메일(%s)? ", old.getEmail())));
m.setPassword(streamTool.promptString("암호? "));
m.setTel(streamTool.promptString(String.format("전화(%s)? ", old.getTel())));
m.setPostNo(streamTool.promptString(String.format("우편번호(%s)? ", old.getPostNo())));
m.setBasicAddress(streamTool.promptString(String.format("기본주소(%s)? ", old.getBasicAddress())));
m.setDetailAddress(streamTool.promptString(String.format("상세주소(%s)? ", old.getDetailAddress())));
m.setWorking(streamTool.promptInt(String.format(
"0. 미취업\n1. 재직중\n재직여부(%s)? ",
old.isWorking() ? "재직중" : "미취업")) == 1);
m.setGender(streamTool.promptInt(String.format(
"0. 남자\n1. 여자\n성별(%s)? ",
old.getGender() == 'M' ? "남자" : "여자")) == 0 ? 'M' : 'W');
m.setLevel((byte) streamTool.promptInt(String.format(
"0. 비전공자\n1. 준전공자\n2. 전공자\n전공(%s)? ",
getLevelText(old.getLevel()))));
String str = streamTool.promptString("정말 변경하시겠습니까?(y/N) ");
if (str.equalsIgnoreCase("Y")) {
// 현재 스레드가 갖고 있는 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
memberDao.update(m);
studentDao.update(m);
con.commit();
streamTool.println("변경했습니다.");
} catch (Exception e) {
con.rollback();
streamTool.println("변경 실패했습니다!");
e.printStackTrace();
} finally {
con.setAutoCommit(true);
}
} else {
streamTool.println("변경 취소했습니다.");
}
streamTool.send();
}
private void deleteMember(StreamTool streamTool) throws Exception {
int memberNo = streamTool.promptInt("회원번호? ");
Student m = this.studentDao.findByNo(memberNo);
if (m == null) {
streamTool.println("해당 번호의 회원이 없습니다.").send();
return;
}
String str = streamTool.promptString("정말 삭제하시겠습니까?(y/N) ");
if (!str.equalsIgnoreCase("Y")) {
streamTool.println("삭제 취소했습니다.").send();
return;
}
// 현재 스레드가 갖고 있는 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
studentDao.delete(memberNo);
memberDao.delete(memberNo);
con.commit();
streamTool.println("삭제했습니다.").send();
} catch (Exception e) {
con.rollback();
streamTool.println("삭제 실패했습니다.").send();
} finally {
con.setAutoCommit(true);
}
}
private void searchMember(StreamTool streamTool) throws Exception {
String keyword = streamTool.promptString("검색어? ");
List<Student> students = this.studentDao.findByKeyword(keyword);
streamTool.println("번호\t이름\t전화\t재직\t전공");
for (Student m : students) {
streamTool.printf("%d\t%s\t%s\t%s\t%s\n",
m.getNo(), m.getName(), m.getTel(),
m.isWorking() ? "예" : "아니오",
getLevelText(m.getLevel()));
}
streamTool.send();
}
public void service(StreamTool streamTool) throws Exception {
menu(streamTool);
while (true) {
String command = streamTool.readString();
if (command.equals("menu")) {
menu(streamTool);
continue;
}
int menuNo;
try {
menuNo = Integer.parseInt(command);
} catch (Exception e) {
streamTool.println("메뉴 번호가 옳지 않습니다!").println().send();
continue;
}
try {
switch (menuNo) {
case 0:
streamTool.println("메인화면으로 이동!").send();
return;
case 1: this.inputMember(streamTool); break;
case 2: this.printMembers(streamTool); break;
case 3: this.printMember(streamTool); break;
case 4: this.modifyMember(streamTool); break;
case 5: this.deleteMember(streamTool); break;
case 6: this.searchMember(streamTool); break;
default:
streamTool.println("잘못된 메뉴 번호 입니다.").send();
}
} catch (Exception e) {
streamTool.printf("명령 실행 중 오류 발생! - %s : %s\n",
e.getMessage(),
e.getClass().getSimpleName()).send();
}
}
}
void menu(StreamTool streamTool) throws Exception {
streamTool.printf("[%s]\n", this.title)
.println("1. 등록")
.println("2. 목록")
.println("3. 조회")
.println("4. 변경")
.println("5. 삭제")
.println("6. 검색")
.println("0. 이전")
.send();
}
}
public class TeacherHandler {
private ConnectionFactory conFactory;
private MemberDao memberDao;
private TeacherDao teacherDao;
private String title;
public TeacherHandler(String title, ConnectionFactory conFactory, MemberDao memberDao, TeacherDao teacherDao) {
this.title = title;
this.conFactory = conFactory;
this.memberDao = memberDao;
this.teacherDao = teacherDao;
}
private void inputTeacher(StreamTool streamTool) throws Exception {
Teacher m = new Teacher();
m.setName(streamTool.promptString("이름? "));
m.setEmail(streamTool.promptString("이메일? "));
m.setPassword(streamTool.promptString("암호? "));
m.setTel(streamTool.promptString("전화? "));
m.setDegree(streamTool.promptInt("1. 고졸\n2. 전문학사\n3. 학사\n4. 석사\n5. 박사\n0. 기타\n학위? "));
m.setSchool(streamTool.promptString("학교? "));
m.setMajor(streamTool.promptString("전공? "));
m.setWage(streamTool.promptInt("강의료(시급)? "));
// 현재 스레드에 보관된 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
memberDao.insert(m);
teacherDao.insert(m);
con.commit();
streamTool.println("입력했습니다!").send();
} catch (Exception e) {
con.rollback();
streamTool.println("입력 실패했습니다!").send();
e.printStackTrace();
} finally {
con.setAutoCommit(true);
}
}
private void printTeachers(StreamTool streamTool) throws Exception {
List<Teacher> teachers = this.teacherDao.findAll();
streamTool.println("번호\t이름\t전화\t학위\t전공\t시강료");
for (Teacher m : teachers) {
streamTool.printf("%d\t%s\t%s\t%s\t%s\t%d\n",
m.getNo(), m.getName(), m.getTel(),
getDegreeText(m.getDegree()), m.getMajor(), m.getWage());
}
streamTool.send();
}
private void printTeacher(StreamTool streamTool) throws Exception {
int teacherNo = streamTool.promptInt("강사번호? ");
Teacher m = this.teacherDao.findByNo(teacherNo);
if (m == null) {
streamTool.println("해당 번호의 강사가 없습니다.").send();
return;
}
streamTool.printf(" 이름: %s\n", m.getName())
.printf(" 이메일: %s\n", m.getEmail())
.printf(" 전화: %s\n", m.getTel())
.printf(" 학위: %s\n", getDegreeText(m.getDegree()))
.printf(" 학교: %s\n", m.getSchool())
.printf(" 전공: %s\n", m.getMajor())
.printf(" 강의료: %s\n", m.getWage())
.printf(" 등록일: %s\n", m.getCreatedDate())
.send();
}
private static String getDegreeText(int degree) {
switch (degree) {
case 1: return "고졸";
case 2: return "전문학사";
case 3: return "학사";
case 4: return "석사";
case 5: return "박사";
default: return "기타";
}
}
private void modifyTeacher(StreamTool streamTool) throws Exception {
int teacherNo = streamTool.promptInt("강사번호? ");
Teacher old = this.teacherDao.findByNo(teacherNo);
if (old == null) {
streamTool.println("해당 번호의 강사가 없습니다.");
return;
}
// 변경할 데이터를 저장할 인스턴스 준비
Teacher m = new Teacher();
m.setNo(old.getNo());
m.setCreatedDate(old.getCreatedDate());
m.setName(streamTool.promptString(String.format("이름(%s)? ", old.getName())));
m.setEmail(streamTool.promptString(String.format("이메일(%s)? ", old.getEmail())));
m.setPassword(streamTool.promptString("암호? "));
m.setTel(streamTool.promptString(String.format("전화(%s)? ", old.getTel())));
m.setDegree(streamTool.promptInt(String.format(
"1. 고졸\n2. 전문학사\n3. 학사\n4. 석사\n5. 박사\n0. 기타\n학위(%s)? ",
getDegreeText(old.getDegree()))));
m.setSchool(streamTool.promptString(String.format("학교(%s)? ", old.getSchool())));
m.setMajor(streamTool.promptString(String.format("전공(%s)? ", old.getMajor())));
m.setWage(streamTool.promptInt(String.format("강의료(시급)(%s)? ", old.getWage())));
String str = streamTool.promptString("정말 변경하시겠습니까?(y/N) ");
if (str.equalsIgnoreCase("Y")) {
// 현재 스레드에 보관된 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
memberDao.update(m);
teacherDao.update(m);
con.commit();
streamTool.println("변경했습니다.");
} catch (Exception e) {
con.rollback();
streamTool.println("변경 실패했습니다.");
e.printStackTrace();
} finally {
con.setAutoCommit(true);
}
} else {
streamTool.println("변경 취소했습니다.");
}
streamTool.send();
}
private void deleteTeacher(StreamTool streamTool) throws Exception {
int teacherNo = streamTool.promptInt("강사번호? ");
Teacher m = this.teacherDao.findByNo(teacherNo);
if (m == null) {
streamTool.println("해당 번호의 강사가 없습니다.").send();
return;
}
String str = streamTool.promptString("정말 삭제하시겠습니까?(y/N) ");
if (!str.equalsIgnoreCase("Y")) {
streamTool.println("삭제 취소했습니다.").send();
return;
}
// 현재 스레드에 보관된 Connection 객체를 리턴 받는다.
Connection con = conFactory.getConnection();
con.setAutoCommit(false);
try {
teacherDao.delete(teacherNo);
memberDao.delete(teacherNo);
streamTool.println("삭제했습니다.").send();
} catch (Exception e) {
con.rollback();
streamTool.println("삭제 실패했습니다.").send();
e.printStackTrace();
} finally {
con.setAutoCommit(true);
}
}
public void service(StreamTool streamTool) throws Exception {
menu(streamTool);
while (true) {
String command = streamTool.readString();
if (command.equals("menu")) {
menu(streamTool);
continue;
}
int menuNo;
try {
menuNo = Integer.parseInt(command);
} catch (Exception e) {
streamTool.println("메뉴 번호가 옳지 않습니다!").println().send();
continue;
}
try {
switch (menuNo) {
case 0:
streamTool.println("메인화면으로 이동!").send();
return;
case 1: this.inputTeacher(streamTool); break;
case 2: this.printTeachers(streamTool); break;
case 3: this.printTeacher(streamTool); break;
case 4: this.modifyTeacher(streamTool); break;
case 5: this.deleteTeacher(streamTool); break;
default:
streamTool.println("잘못된 메뉴 번호 입니다.").send();
}
} catch (Exception e) {
streamTool.printf("명령 실행 중 오류 발생! - %s : %s\n",
e.getMessage(),
e.getClass().getSimpleName()).send();
}
}
}
void menu(StreamTool streamTool) throws Exception {
streamTool.printf("[%s]\n", this.title)
.println("1. 등록")
.println("2. 목록")
.println("3. 조회")
.println("4. 변경")
.println("5. 삭제")
.println("0. 이전")
.send();
}
}
40. DB 커넥션 풀 도입
C1 에서 ConnectionPool 에 ① getConnection() 한다. Connection con1 을 ② 생성해서 주면 C1 에서 ③ 사용한다. con1 사용 후 ④ returnConnection() 해서 ConnectionPool 에 반납하면 여기서 List 에 ⑤ 보관한다.
C2 에서 ConnectionPool 에 ① getConnection() 한다. ConnectionPool 의 List 에 보관된 것(con1)이 있으면 ② 꺼낸다. ③ 기존 객체 리턴 (con1)하면 C2 가 ④ 사용한다. 사용 후 ⑤ returnConnection() 하면 ConnectionPool 객체가 List 에 ⑥ 보관한다.
☆Pooling 기법
① 객체 생성 시간이 많이 소요되는 경우
② 객체가 무거운 경우(메모리 많이 사용)
생성 → 사용 → 가비지 ↔ 보관, 꺼내기
ConnectionPool의 사용
C1 에서 ConnectionFactory 객체 통해 getConnection() 한다. 여기서 ConnectionPool 객체 통해 getConnection() 한다. 커넥션 없으면 커넥션 생성하고 있으면 기존의 커넥션 리턴 한다.
con 사용 후 ① C1 에서 ConnectionFactory 객체 통해 closeConnection() 하면 여기서 ConnectionPool 객체 통해 ② returnConnection() 한다. ConnectionPool 객체에서 List 에 ③ 보관 한다.
→ ConnectionFactory, ConnectionPool 을 "DataSource" 라 한다.
### 40. DB 커넥션 풀을 이용한 Connetion 재사용
- 풀링기법(pooling)을 이용하여 Connection 객체를 재사용하는 방법
- GoF의 'Flyweight' 디자인 패턴
public class ConnectionPool {
String jdbcUrl;
String username;
String password;
List<Connection> conList = new ArrayList<>();
public ConnectionPool(String jdbcUrl, String username, String password) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
}
public Connection getConnection() throws Exception {
if (conList.size() == 0) {
System.out.println("Connection 생성!");
return DriverManager.getConnection(jdbcUrl, username, password);
}
return conList.remove(0);
}
public void returnConnection(Connection con) {
conList.add(con);
}
}
public class ConnectionFactory {
ConnectionPool connectionPool;
ThreadLocal<Connection> conLocal = new ThreadLocal<>();
public ConnectionFactory(ConnectionPool connectionPool) {
this.connectionPool = connectionPool;
}
public Connection getConnection() throws Exception {
// 현재 이 명령을 실행하는 스레드에 마련된 conLocal 변수에서 값을 꺼낸다.
Connection con = conLocal.get();
if (con == null) {
// 현재 스레드 변수에 Connection 객체가 들어 있지 않다면 커넥션풀에서 가져온다.
con = connectionPool.getConnection(); // 새로 커넥션을 생성하거나 기존 커넥션을 리턴한다.
// 현재 스레드가 다음에도 사용할 수 있도록 Connection 객체를 스레드 변수에 보관한다.
conLocal.set(con);
}
System.out.printf("[%s] Connection 객체 리턴!\n", Thread.currentThread().getName());
// 현재 스레드를 위해 준비한 Connection 객체를 리턴한다.
return con;
}
public void closeConnection() {
// 현재 스레드가 사용한 커넥션 객체를 꺼내 ConnectionPool에 반납한다.
Connection con = conLocal.get();
if (con != null) {
connectionPool.returnConnection(con);
System.out.printf("[%s] 커넥션 객체 반납했습니다.\n", Thread.currentThread().getName());
}
}
}
public class ServerApp {
ConnectionPool connectionPool = new ConnectionPool(
"jdbc:mariadb://localhost:3306/studydb", "study", "1111");
ConnectionFactory conFactory = new ConnectionFactory(connectionPool);
StudentHandler studentHandler;
TeacherHandler teacherHandler;
BoardHandler boardHandler;
HelloHandler helloHandler = new HelloHandler();
public static void main(String[] args) {
try {
new ServerApp().execute(8888);
} catch (Exception e) {
System.out.println("서버 실행 오류!");
e.printStackTrace();
}
}
public ServerApp() throws Exception{
BoardDaoImpl boardDao = new BoardDaoImpl(conFactory);
MemberDaoImpl memberDao = new MemberDaoImpl(conFactory);
StudentDaoImpl studentDao = new StudentDaoImpl(conFactory);
TeacherDaoImpl teacherDao = new TeacherDaoImpl(conFactory);
this.studentHandler = new StudentHandler("학생", conFactory, memberDao, studentDao);
this.teacherHandler = new TeacherHandler("강사", conFactory, memberDao, teacherDao);
this.boardHandler = new BoardHandler("게시판", boardDao);
}
void execute(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중...");
while (true) {
Socket socket = serverSocket.accept();
new Thread(() -> service(socket)).start();
}
} catch (Exception e) {
System.out.println("서버 소켓 오류!");
e.printStackTrace();
}
}
private void hello(StreamTool streamTool) throws Exception {
streamTool.println("비트캠프 관리 시스템")
.println(" Copyright by 네이버클라우드1기")
.println("--------------------------------------")
.println("안녕하세요!")
.println()
.send();
}
private void processRequest(StreamTool streamTool) throws Exception {
loop: while (true) {
String command = streamTool.readString();
if (command.equals("menu")) {
menu(streamTool);
continue;
}
int menuNo;
try {
menuNo = Integer.parseInt(command);
} catch (Exception e) {
streamTool.println("메뉴 번호가 옳지 않습니다!").println().send();
continue;
}
try {
switch (menuNo) {
case 1:
studentHandler.service(streamTool);
break;
case 2:
teacherHandler.service(streamTool);
break;
case 3:
boardHandler.service(streamTool);
break;
case 4:
helloHandler.service(streamTool);
break;
case 9:
break loop; // loop 라벨이 붙은 while 문을 나간다.
default:
streamTool.println("잘못된 메뉴 번호 입니다.").send();
}
} catch (Exception e) {
streamTool.printf("명령 실행 중 오류 발생! - %s : %s\n",
e.getMessage(),
e.getClass().getSimpleName()).send();
}
}
// while 종료하면 클라이언트와 연결을 끊는다.
streamTool.print("quit").send();
}
void menu(StreamTool streamTool) throws Exception {
streamTool.println("1. 학생관리")
.println("2. 강사관리")
.println("3. 게시판")
.println("4. 인사")
.println("9. 종료")
.println("메뉴 번호:")
.send();
}
public void service(Socket clientSocket) {
// 스레드가 실행할 코드를 둔다.
try (Socket socket = clientSocket;
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream())) {
// 입출력 보조 도구 준비
StreamTool streamTool = new StreamTool(in, out);
String clientIP = socket.getInetAddress().getHostAddress();
System.out.printf("접속: %s\n", clientIP);
hello(streamTool);
processRequest(streamTool);
System.out.printf("끊기: %s\n", clientIP);
} catch (Exception e) {
System.out.println("클라이언트 요청 처리 오류!");
e.printStackTrace();
} finally {
// 현재 스레드가 갖고 있는 커넥션 객체를 ConnectionPool에 반납시킨다.
conFactory.closeConnection();
}
}
}
41. PreparedStatement를 이용한 SQL 삽입 공격 막기
### 41. PreparedStatement를 이용한 SQL 삽입 공격 막기
- SQL 삽입 공격 체험
- SQL 삽입 공격을 막는 방법
PreparedStatement 로 SQL 문을 미리 준비해 놓는다.
public class BoardDaoImpl implements BoardDao {
ConnectionFactory conFactory;
public BoardDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Board b) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"insert into app_board(title, content, pwd) values(?, ?, ?)")) {
stmt.setString(1, b.getTitle());
stmt.setString(2, b.getContent());
stmt.setString(3, b.getPassword());
stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Board> findAll() {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select board_id, title, created_date, view_cnt from app_board order by board_id desc");
ResultSet rs = stmt.executeQuery()) {
ArrayList<Board> list = new ArrayList<>();
while (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
list.add(b);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Board findByNo(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select board_id, title, content, pwd, created_date, view_cnt from app_board where board_id=?")) {
stmt.setInt(1, no);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setContent(rs.getString("content"));
b.setPassword(rs.getString("pwd"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
return b;
}
return null;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public void increaseViewCount(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"update app_board set"
+ " view_cnt = view_cnt + 1"
+ " where board_id=?")) {
stmt.setInt(1, no);
stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Board> findByKeyword(String keyword) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select board_id, title, created_date, view_cnt"
+ " from app_board"
+ " where title like(?)"
+ " or content like(?)"
+ " order by board_id desc")) {
stmt.setString(1, "%" + keyword + "%");
stmt.setString(2, "%" + keyword + "%");
try (ResultSet rs = stmt.executeQuery()) {
ArrayList<Board> list = new ArrayList<>();
while (rs.next()) {
Board b = new Board();
b.setNo(rs.getInt("board_id"));
b.setTitle(rs.getString("title"));
b.setCreatedDate(rs.getString("created_date"));
b.setViewCount(rs.getInt("view_cnt"));
list.add(b);
}
return list;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Board b) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"update app_board set title=?, content=? where board_id=?")) {
// 미리 준비한 SQL에 값만 별도록 설정한다.
// 이때 문자열 안에 들어 있는 '(작은 따옴표)는 일반 문자로 간주한다.
// 따라서 SQL 삽입 공격이 불가능 하다!
//
stmt.setString(1, b.getTitle());
stmt.setString(2, b.getContent());
stmt.setInt(3, b.getNo());
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"delete from app_board where board_id=?")) {
stmt.setInt(1, no);
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class MemberDaoImpl implements MemberDao {
ConnectionFactory conFactory;
public MemberDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Member m) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"insert into app_member(name, email, pwd, tel)"
+ " values(?, ?, sha2(?,256), ?)", Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, m.getName());
stmt.setString(2, m.getEmail());
stmt.setString(3, m.getPassword());
stmt.setString(4, m.getTel());
stmt.executeUpdate();
try (ResultSet keyRs = stmt.getGeneratedKeys()) {
keyRs.next();
int autoGeneratedMemberId = keyRs.getInt(1);
m.setNo(autoGeneratedMemberId);
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Member> findAll() {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select member_id, name, email, created_date"
+ " from app_member"
+ " order by member_id desc");
ResultSet rs = stmt.executeQuery()) {
ArrayList<Member> list = new ArrayList<>();
while (rs.next()) {
Member m = new Member();
m.setNo(rs.getInt("member_id"));
m.setName(rs.getString("name"));
m.setEmail(rs.getString("email"));
m.setCreatedDate(rs.getDate("created_date"));
list.add(m);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Member findByNo(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select member_id, name, email, tel, created_date"
+ " from app_member"
+ " where member_id=?")) {
stmt.setInt(1, no);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Member m = new Member();
m.setNo(rs.getInt("member_id"));
m.setName(rs.getString("name"));
m.setEmail(rs.getString("email"));
m.setTel(rs.getString("tel"));
m.setCreatedDate(rs.getDate("created_date"));
return m;
}
return null;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Member m) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"update app_member set "
+ " name=?, email=?, pwd=sha2(?,256), tel=?"
+ " where member_id=?")) {
stmt.setString(1, m.getName());
stmt.setString(2, m.getEmail());
stmt.setString(3, m.getPassword());
stmt.setString(4, m.getTel());
stmt.setInt(5, m.getNo());
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"delete from app_member"
+ " where member_id=?")) {
stmt.setInt(1, no);
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class StudentDaoImpl implements StudentDao {
ConnectionFactory conFactory;
public StudentDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Student s) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"insert into app_student("
+ " member_id,"
+ " pst_no,"
+ " bas_addr,"
+ " det_addr,"
+ " work,"
+ " gender,"
+ " level)"
+ " values(?,?,?,?,?,?,?)")) {
stmt.setInt(1, s.getNo());
stmt.setString(2, s.getPostNo());
stmt.setString(3, s.getBasicAddress());
stmt.setString(4, s.getDetailAddress());
stmt.setBoolean(5, s.isWorking());
stmt.setString(6, String.valueOf(s.getGender()));
stmt.setInt(7, s.getLevel());
stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Student> findAll() {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " s.work,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " order by"
+ " m.name asc");
ResultSet rs = stmt.executeQuery()) {
ArrayList<Student> list = new ArrayList<>();
while (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setWorking(rs.getBoolean("work"));
s.setLevel(rs.getByte("level"));
list.add(s);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Student findByNo(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " m.created_date,"
+ " s.pst_no,"
+ " s.bas_addr,"
+ " s.det_addr,"
+ " s.work,"
+ " s.gender,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " where s.member_id=?")) {
stmt.setInt(1, no);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setCreatedDate(rs.getDate("created_date"));
s.setPostNo(rs.getString("pst_no"));
s.setBasicAddress(rs.getString("bas_addr"));
s.setDetailAddress(rs.getString("det_addr"));
s.setWorking(rs.getBoolean("work"));
s.setGender(rs.getString("gender").charAt(0));
s.setLevel(rs.getByte("level"));
return s;
}
return null;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Student> findByKeyword(String keyword) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " s.work,"
+ " s.level"
+ " from app_student s"
+ " inner join app_member m on s.member_id = m.member_id"
+ " where"
+ " m.name like(?)"
+ " or m.email like(?)"
+ " or m.tel like(?)"
+ " or s.bas_addr like(?)"
+ " or s.det_addr like(?)"
+ " order by"
+ " m.member_id desc")) {
stmt.setString(1, "%" + keyword + "%");
stmt.setString(2, "%" + keyword + "%");
stmt.setString(3, "%" + keyword + "%");
stmt.setString(4, "%" + keyword + "%");
stmt.setString(5, "%" + keyword + "%");
try (ResultSet rs = stmt.executeQuery()) {
ArrayList<Student> list = new ArrayList<>();
while (rs.next()) {
Student s = new Student();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setWorking(rs.getBoolean("work"));
s.setLevel(rs.getByte("level"));
list.add(s);
}
return list;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Student s) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"update app_student set "
+ " pst_no=?,"
+ " bas_addr=?,"
+ " det_addr=?,"
+ " work=?,"
+ " gender=?,"
+ " level=? "
+ " where member_id=?")) {
stmt.setString(1, s.getPostNo());
stmt.setString(2, s.getBasicAddress());
stmt.setString(3, s.getDetailAddress());
stmt.setBoolean(4, s.isWorking());
stmt.setString(5, String.valueOf(s.getGender()));
stmt.setInt(6, s.getLevel());
stmt.setInt(7, s.getNo());
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"delete from app_student"
+ " where member_id=?")) {
stmt.setInt(1, no);
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
}
public class TeacherDaoImpl implements TeacherDao {
ConnectionFactory conFactory;
public TeacherDaoImpl(ConnectionFactory conFactory) {
this.conFactory = conFactory;
}
@Override
public void insert(Teacher s) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"insert into app_teacher("
+ " member_id,"
+ " degree,"
+ " school,"
+ " major,"
+ " wage)"
+ " values(?, ?, ?, ?, ?)")) {
stmt.setInt(1, s.getNo());
stmt.setInt(2, s.getDegree());
stmt.setString(3, s.getSchool());
stmt.setString(4, s.getMajor());
stmt.setInt(5, s.getWage());
stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public List<Teacher> findAll() {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select"
+ " m.member_id,"
+ " m.name,"
+ " m.tel,"
+ " t.degree,"
+ " t.major,"
+ " t.wage"
+ " from app_teacher t"
+ " inner join app_member m on t.member_id = m.member_id"
+ " order by"
+ " m.name asc");
ResultSet rs = stmt.executeQuery()) {
ArrayList<Teacher> list = new ArrayList<>();
while (rs.next()) {
Teacher s = new Teacher();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setTel(rs.getString("tel"));
s.setDegree(rs.getInt("degree"));
s.setMajor(rs.getString("major"));
s.setWage(rs.getInt("wage"));
list.add(s);
}
return list;
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public Teacher findByNo(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"select"
+ " m.member_id,"
+ " m.name,"
+ " m.email,"
+ " m.tel,"
+ " m.created_date,"
+ " t.degree,"
+ " t.school,"
+ " t.major,"
+ " t.wage"
+ " from app_teacher t"
+ " inner join app_member m on t.member_id = m.member_id"
+ " where m.member_id=?")) {
stmt.setInt(1, no);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Teacher s = new Teacher();
s.setNo(rs.getInt("member_id"));
s.setName(rs.getString("name"));
s.setEmail(rs.getString("email"));
s.setTel(rs.getString("tel"));
s.setCreatedDate(rs.getDate("created_date"));
s.setDegree(rs.getInt("degree"));
s.setSchool(rs.getString("school"));
s.setMajor(rs.getString("major"));
s.setWage(rs.getInt("wage"));
return s;
}
return null;
}
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int update(Teacher t) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"update app_teacher set "
+ " degree=?,"
+ " school=?,"
+ " major=?,"
+ " wage=?"
+ " where member_id=?")) {
stmt.setInt(1, t.getDegree());
stmt.setString(2, t.getSchool());
stmt.setString(3, t.getMajor());
stmt.setInt(4, t.getWage());
stmt.setInt(5, t.getNo());
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
@Override
public int delete(int no) {
try (PreparedStatement stmt = conFactory.getConnection().prepareStatement(
"delete from app_teacher"
+ " where member_id=?")) {
stmt.setInt(1, no);
return stmt.executeUpdate();
} catch (Exception e) {
throw new DaoException(e);
}
}
}
Mybatis API 사용 준비
아래 사이트에서 xml 소스 코드 가져온다.
mybatis.org > Github Project > mybatis-3
myapp-server에 mybatis 라이브러리 추가
app-server > build.gradle 에 아래 코드 추가한다.
dependencies {
// Mybatis SQL Mapper 라이브러리
implementation 'org.mybatis:mybatis:3.5.11'
https://central.sonatype.com/ > org.mybatis 검색해서 Gradle (short) 복사한 것이다.
명령창에 $ gradle eclipse 하면 다운로드 한다.
Referenced Libraries 에 mybatis-3.5.11.jar 추가된 것 확인된다.
mybatis.org 에서 github 으로 가서 mybatis.github.io/mybatis-3/ 에서 아래 페이지 접근 가능하다.
42. Mybatis SQL 매퍼 사용하기
### 42. Mybatis SQL 매퍼 사용하기
- Mybatis SQL 매퍼를 사용하여 DAO를 구현하는 방법
ServerApp.java 에 아래 처럼 입력한다.
public class ServerApp {
ConnectionPool connectionPool = new ConnectionPool(
"jdbc:mariadb://localhost:3306/studydb", "study", "1111");
ConnectionFactory conFactory = new ConnectionFactory(connectionPool);
StudentHandler studentHandler;
TeacherHandler teacherHandler;
BoardHandler boardHandler;
HelloHandler helloHandler = new HelloHandler();
public static void main(String[] args) {
try {
new ServerApp().execute(8888);
} catch (Exception e) {
System.out.println("서버 실행 오류!");
e.printStackTrace();
}
}
public ServerApp() throws Exception{
// Mybatis API 사용 준비
// 1) Mybatis 설정파일 준비
// => resources/bitcamp/myapp/config/mybatis-config.xml
// 2) SQL Mapper 파일 준비
// => resources/bitcamp/myapp/mapper/BoardMapper.xml
// 3) Mybatis 설정 파일을 읽을 때 사용할 입력 스트림 객체 준비
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
// Resources.getResourceAsStream : 경로 기준은 classpath 이다.
// classpath : myapp-server/bin/main
// 4) SqlSessionFactoryBuilder 객체 준비
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 5) builder를 이용하여 SqlSessionFactory 객체 생성
SqlSessionFactory sqlSessionFactory = builder.build(mybatisConfigInputStream);
BoardDaoImpl boardDao = new BoardDaoImpl(sqlSessionFactory);
MemberDaoImpl memberDao = new MemberDaoImpl(conFactory);
StudentDaoImpl studentDao = new StudentDaoImpl(conFactory);
TeacherDaoImpl teacherDao = new TeacherDaoImpl(conFactory);
this.studentHandler = new StudentHandler("학생", conFactory, memberDao, studentDao);
this.teacherHandler = new TeacherHandler("강사", conFactory, memberDao, teacherDao);
this.boardHandler = new BoardHandler("게시판", boardDao);
}
Mybatis API 사용 준비
1) Mybatis 설정파일 준비
mybatis.org 사이트에서 아래 부분 복사해서 사용한다.
src/main 에 resources 폴더 만들고 $ gradle eclipse 하면 이클립스 프로젝트 폴더로 인식한다.
mybatis-config.xml 파일 저장 위치: src.main.resources.bitcamp.myapp.config.mybatis-config.xml
mybatis-config.xml
environments default 에 "development" 입력하면 environment id 가 "development" 인 것을 사용한다. 이런식으로 설정 여러개 해놓고 DB 골라서 사용할 수 있다.
아래 <mapper resources=" "> 에 BoardMapper.xml 경로 입력한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.mariadb.jdbc.Driver"/>
<property name="url" value="jdbc:mariadb://localhost:3306/studydb"/>
<property name="username" value="study"/>
<property name="password" value="1111"/>
</dataSource>
</environment>
<mappers>
<mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
</mappers>
</configuration>
복사한 코드
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
driver 주소는 myapp-server > Referenced Libraries > mariadb-java-client-3.1.2.jar > META-INF > services > java.sql.Driver 에 있다.
2) SQL Mapper 파일 준비
boardmapper 는 mybatis.org 사이트에서 아래 코드 복사한다.
BoardMapper.xml 파일 저장 위치: src.main.resources.bitcamp.myapp.mapper.BoardMapper.xml
BoardMapper.xml
namespace.id (BoardMapper.findAll) 처럼 사용한다.
예) sqlSession.selectList("BoardMapper.findAll");
insert 에서 #{title} 로 사용시 parameterType 에 해당하는 클래스 파일에서 프로퍼티 찾아서 값 입력해준다.
select 에서 select 문으로 가져온 데이터를 resultType 의 클래스의 객체에 넣어준다. DB 에 저장한 컬럼명을 찾아서 객체 프로퍼티와 동일한 필드에 넣는다. DB 와 객체 프로퍼티가 다르면 select 문에 as 로 객체 프로퍼티와 동일한 이름으로 별명 붙여준다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardMapper">
<insert id="insert" parameterType="bitcamp.myapp.vo.Board">
insert into app_board(title, content, pwd)
values(#{title}, #{content}, #{password})
</insert>
<select id ="findAll" resultType="bitcamp.myapp.vo.Board">
select
board_id as no,
title,
created_date as createdDate,
view_cnt as viewCount
from
app_board
order by
board_id desc
</select>
</mapper>
3) Mybatis 설정 파일을 읽을 때 사용할 입력 스트림 객체 준비
mybatis.org 에서 Building SqlSessionFactory from XML 코드 중 해당 부분 가져온다.
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
// Resources.getResourceAsStream : 경로 기준은 classpath 이다.
// classpath : myapp-server > bin > main
4) SqlSessionFactoryBuilder 객체 준비
5) builder를 이용하여 SqlSessionFactory 객체 생성
sqlSessionFactoryBuilder 객체 생성하고 메서드 build() 매개변수에 3) 에서 선언한 입력 스트림 객체를 넣는다.
// 4) SqlSessionFactoryBuilder 객체 준비
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 5) builder를 이용하여 SqlSessionFactory 객체 생성
SqlSessionFactory sqlSessionFactory = builder.build(mybatisConfigInputStream);
사이트 에서는 한 줄에 입력하였다.
BoardDaoImpl.java 에서 sqlSessionFactory 사용해서 insert(), findAll() 코드 변경한다.
기본 autoCommit = false 라 insert() 에서 commit() 해야 반영된다.
public class BoardDaoImpl implements BoardDao {
ConnectionFactory conFactory;
SqlSessionFactory sqlSessionFactory;
public BoardDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void insert(Board b) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.insert("BoardMapper.insert", b);
sqlSession.commit();
}
}
@Override
public List<Board> findAll() {
// openSession() 에서 던지는 Exception 이 runtime Exception 이다.
// 자동 close 하려고 try 문 쓴다.
// SqlSession 은 스레드에 종속된 커넥션을 사용하므로 공유하면 안된다.
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("BoardMapper.findAll");
}
}
sqlSession.update() 이런 식으로 사용하면 Mybatis 내부에서 JDBC 사용해서 통신한다.
org.apache.ibatis.executor.statement.SimpleStatementHandler 코드 참고
public class SimpleStatementHandler extends BaseStatementHandler {
public SimpleStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
@Override
public int update(Statement statement) throws SQLException {
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
int rows;
if (keyGenerator instanceof Jdbc3KeyGenerator) {
statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
rows = statement.getUpdateCount();
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else if (keyGenerator instanceof SelectKeyGenerator) {
statement.execute(sql);
rows = statement.getUpdateCount();
keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
} else {
statement.execute(sql);
rows = statement.getUpdateCount();
}
return rows;
}
조언
*자동차를 탄다고 모든 기능을 공부하고 타지 않는다. 정비공이 되려면 정비공이 되기 위해 필요한 지식을 배우면 된다. 엔진 연구원이 되려면 엔진 연구하기 위해 필요한 지식을 배우면 된다.
*객체지향 프로그램 잘짜는 방법: 객체가 어떤 역할 하는지에만 집중하라. 자세한 동작 원리를 너무 깊게 파고들지 말라.
과제
/
'네이버클라우드 AIaaS 개발자 양성과정 1기 > DBMS, SQL, JDBC, Servlet' 카테고리의 다른 글
[JDBC] 예제 소스 정리 - Mybatis (0) | 2023.02.16 |
---|---|
[비트캠프] 71일차(15주차3일) - JDBC(MyBatis), myapp-42~43(SqlSession, 프록시 패턴) (0) | 2023.02.15 |
[비트캠프] 69일차(15주차1일) - JDBC: myapp-38 (0) | 2023.02.13 |
[비트캠프] 68일차(14주차5일) - JDBC: myapp-36~37 (0) | 2023.02.10 |
[SQL] 예제 소스 정리 - JDBC (0) | 2023.02.09 |