네이버클라우드 AIaaS 개발자 양성과정 1기/DBMS, SQL, JDBC, Servlet

[비트캠프] 71일차(15주차3일) - JDBC(MyBatis), myapp-42~43(SqlSession, 프록시 패턴)

끈기JK 2023. 2. 15. 20:51

 

Mybatis 사용법 : SqlSession 객체

 

처음엔 아래 5개 메서드만 알면 된다.

sqlSession.

selectOne() → select 문 : 0 또는 1개 결과. 만약 2개 이상의 결과를 리턴하는 select 문을 실행하면 => 예외 발생!

selectList() → select 문 : 0개 이상의 결과

insert() 

update()

delete()

→ insert/update/delete 문. 메서드와 상관없이 실행가능! → JDBC API 에서도 executeUpdate() 로 모두 실행!

     내부적으로 executeUpdate() 를 call 한다.

 

 

 

Mybatis 사용법 : 메서드와 SQLID

 

자바코드 : List<Board> list = sqlSession.selectList("BoardMapper.findAll"); 에서 BoardMapper.findAll 을 SQL 아이디라 한다.

BoardMapper → <mapper namespace="BoardMapper"> 이다.

 

BoardMapper.xml 코드 <select id ="findAll" resultType="bitcamp.myapp.vo.Board"> 에서

Board → bitcamp.myapp.vo.Board 이고

findAll → id="findAll 이다.

 

 

 

Mybatis 사용법 : 컬럼과 프로퍼티

 

<select id="findAll" resultType="----Board">

  select

    board_id,          → setBoard_id() 호출

    title,                   → setTitle() 호출

    created_date,   → setCreated_date() 호출

    view_cnt           → setView_cnt() 호출

                                   ↑ 밑줄 친 세터 메서드는 존재하지 않기 때문에 해당 컬럼의 값을 자바 객체에 담지 않는다.

                                                  (필드(field), 프로퍼티(property))

해결책!

별명 부여 => board_id as no,  → setNo() 호출. 별명과 일치하는 프로퍼티를 찾는다.

 

 

 

Mybatis 사용법 : 결과 레코드와 자바 객체

 

Board 클래스의 인스턴스를 UML 로 표기하는 방법은 위와 같다.

 

결과 레코드를 자바 객체로 받는 방법인 selectList() 의 원리이다.

각 결과 레코드의 board_id, title, created_date, view_cnt 를 각 Board 인스턴스로 저장한다.

이들의 주소를 List 에 저장해서 return 한다.

 

 

 

Mybatis 사용법 : in-parameter 와 메서드 파라미터

 

sqlSession.insert("BoardMapper.insert", b); 에서 b 는 Board 인스턴스이다.

BoardMapper 를 읽고 namespace="BoardMapper" 인 코드를 찾는다.

 

<insert id="insert" parameterType="bitcamp.myapp.vo.Board">  // Board : 파라미터로 넘어오는 객체의 타입(클래스)

  insert into app_board(title, content, pwd)

  values(#{title}, #{content}, #{password})  // b.getTitle() 의 리턴값, b.getContent() 의 리턴값, b.getPassword() 의 리턴 값

 

※ 주의!

In-parameter 는 타입에 상관없이 #{프로퍼티명} 으로 값을 삽입한다. 문자열이라고 해서 '#{...}' 양쪽에 작은 따옴표는 붙여서는 안된다.

 

 

 

Mybatis 사용법 : in-parameter 와 자바 원시타입, String

 

sqlSession.selectOne("BoardMapper.findByNo", 20);  // int 타입

<insert id="insert" parameterType="int">  // "_int" 도 상관없다.

int : Integer

_int : int   ← primitive type 인 경우. 예) int 또는 _int 상관 없다.

  ...

  where board_id = #{haha | ok | value}  // primitive type 이나 String 타입인 경우 아무 이름을 사용해도 된다.

 

 

 

DAO 와 Transaction

 

Handler 에서 ① insert() 를 MemberDao 객체에서 한다. 여기서 DBMS 로 insert 한다.

Handler 에서 ② insert() 를 StudentDao 객체에서 한다. ①, ② 를 1개의 트랜잭션으로 묶는다.

 

insert 가 무조건 개별적으로 실행된다면 insert 후에 commit 하는 것이 맞다.

그런데 다른 변경작업과 묶일 수 있는 경우라면 개별적으로 commit/rollback 해서는 안된다.

결론

→ 여러 DAO 작업을 한 개의 트랜잭션으로 묶어서 사용하는 경우도 있기 때문에 DAO 에서 commit/rollback 하지 말라! → commit/rollback 은 DAO 를 사용하는 측에서 제어해야 한다!

 

 

 

Mybatis 를 사용하는 DAO 의 트랜잭션 제어

 

① 이전 방식

Handler 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴받는다. setAutoCommit(false) - commit(), rollback() 한다.

MemberDao 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴 받아 사용한다.

StudentDao 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴 받아 사용한다.

 

Handler 에 Connection 객체를 제어한다. DAO 들은 Handler 가 사용하는 Connection 을 공유한다.

 

 

 

Mybatis를 사용하는 DAO 의 트랜잭션 제어

 

② 변경 방식

Handler 에서 SqlSession 에 대해

① 트랜잭션 시작

② MemberDao 객체 insert() → SqlSession 객체 사용

③ StudentDao 객체 insert() → SqlSession 객체 사용

→ 같은 트랜잭션으로 묶인 경우 같은 SqlSession 객체를 사용하도록 제어하면 된다.

④ commit()/rollback()

 

 

 

SqlSession 의 트랜잭션 제어

 

Handler 에서 TransactionManager 객체 생성하고 startTransaction() 실행한다. 여기서 SqlSessionFactory (BitcampSqlSessionFactory) 의 prepareSqlSessionForThread() 실행한다. 그러면 Thread 변수에 SqlSession 기록한다.

MemberDao 에서 txManager의 openSession() 실행한다. SqlSessionFactory 에서 get() 으로 SqlSession 객체 가져오고 스레드에 보관된 SqlSession 리턴한다.

StudentDao 에서 txManager 의 openSession() 실행한다. SqlSessionFactory 에서 get() 으로 SqlSession 객체 가져오고 스레드에 보관된 sqlSession 객체 리턴한다.

Handler 에서 commit()/rollback() 한다.

 

 

 

SqlSessionFactory 클래스의 기능 변경(추가, 변경, 삭제)

 

→ 기존 클래스의 코드를 손대지 않고 기능 변경 (상속 | Proxy)

사용자가 BitcampSqlSessionFactory 이용해서 요청한다. 이 객체는 《interface》SqlSessionFactory 구현체이고 proxy 이다. MyBatis 구현체에서 +α 하기 위해 작성하였다. 여기서 Mybatis 구현체에 위임한다.

 

 

 

42. Mybatis SQL 매퍼 사용하기(트랜잭션 제어 포함) 

 

### 42. Mybatis SQL 매퍼 사용하기(트랜잭션 제어 포함) 
- Mybatis SQL 매퍼를 사용하여 DAO를 구현하는 방법 
- Mybatis 를 이용하여 트랜잭션을 제어하는 방법 
- Proxy 패턴으로 기존 클래스의 기능을 변경하는 방법

 

----DaoImpl 생성자에 SqlSessionFactory 객체를 받는다.

try 문에 connection 객체를 얻어서 prepareStatement 를 준비하는 대신, sqlSessionFactory 에서 openSession() 실행해서 SqlSession 객체를 준비한다.

SQL 문을 java 에 직접 작성하지 않고 별로도 작성된 xml 파일을 불러오는 방식을 사용한다.

public class BoardDaoImpl implements BoardDao {

  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);
    }
  }

  @Override
  public List<Board> findAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("BoardMapper.findAll");
    }
  }

  @Override
  public Board findByNo(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectOne("BoardMapper.findByNo", no);
    }
  }

  @Override
  public void increaseViewCount(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      sqlSession.update("BoardMapper.increaseViewCount", no);
    }
  }

  @Override
  public List<Board> findByKeyword(String keyword) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("BoardMapper.findByKeyword", keyword);
    }
  }

  @Override
  public int update(Board b) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.update("BoardMapper.update", b);
    }
  }

  @Override
  public int delete(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("BoardMapper.delete", no);
    }
  }
}
public class MemberDaoImpl implements MemberDao {

  SqlSessionFactory sqlSessionFactory;

  public MemberDaoImpl(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  @Override
  public void insert(Member m) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      sqlSession.insert("MemberMapper.insert", m);
    }
  }

  @Override
  public List<Member> findAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("MemberMapper.findAll");
    }
  }

  @Override
  public Member findByNo(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectOne("MemberMapper.findByNo", no);
    }
  }

  @Override
  public int update(Member m) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.update("MemberMapper.update", m);
    }
  }

  @Override
  public int delete(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("MemberMapper.delete", no);
    }
  }
}
public class StudentDaoImpl implements StudentDao {

  SqlSessionFactory sqlSessionFactory;

  public StudentDaoImpl(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  @Override
  public void insert(Student s) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      sqlSession.insert("StudentMapper.insert", s);
    }
  }

  @Override
  public List<Student> findAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("StudentMapper.findAll");
    }
  }

  @Override
  public Student findByNo(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectOne("StudentMapper.findByNo", no);
    }
  }

  @Override
  public List<Student> findByKeyword(String keyword) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("StudentMapper.findByKeyword");
    }
  }

  @Override
  public int update(Student s) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.update("StudentMapper.update", s);
    }
  }

  @Override
  public int delete(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("StudentMapper.delete", no);
    }
  }
}
public class TeacherDaoImpl implements TeacherDao {

  SqlSessionFactory sqlSessionFactory;

  public TeacherDaoImpl(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  @Override
  public void insert(Teacher t) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      sqlSession.insert("TeacherMapper.insert", t);
    }
  }

  @Override
  public List<Teacher> findAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("TeacherMapper.findAll");
    }
  }

  @Override
  public Teacher findByNo(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectOne("TeacherMapper.findByNo", no);
    }
  }

  @Override
  public int update(Teacher t) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.update("TeacherMapper.update", t);
    }
  }

  @Override
  public int delete(int no) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.delete("TeacherMapper.delete", no);
    }
  }
}

 

BoardMapper.xml 파일에 sqlSession 에서 지정한 명령 작성되어 있다.

위에서 insert, update, delete 로 매개변수 전달시 타입을 parameterType=" " 에 지정한다.

SQL 문에서 select 로 가져오는 column 명이 프로퍼티 명과 다르면 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>

  <select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Board">
    select 
      board_id as no, 
      title, 
      content, 
      pwd as password, 
      created_date as createdDate, 
      view_cnt as viewCount
    from 
      app_board 
    where 
      board_id=#{no}
  </select>
  
  <update id="increaseViewCount" parameterType="int">
    update app_board set
      view_cnt = view_cnt + 1
    where board_id=#{maumdaerohaedodoi}
  </update>
  
  <select id="findByKeyword" parameterType="string" resultType="bitcamp.myapp.vo.Board">
    select 
      board_id as no, 
      title, 
      created_date as createdDate, 
      view_cnt as viewCount 
    from 
      app_board 
    where 
      title like(concat('%',#{keyword},'%'))
      or content like(concat('%',#{keyword},'%'))
    order by 
      board_id desc
  </select>
  
  <update id="update" parameterType="bitcamp.myapp.vo.Board">
    update app_board set 
      title=#{title}, 
      content=#{content} 
    where board_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_board 
    where board_id=#{no}
  </delete>
</mapper>

 

MemberMapper.xml 은 generatedKey 를 객체에 저장해야 한다. 그래야 그 다음 studentDao.insert(s) 객체의 pk(=fk) 인 member_id 에 memberDao 에서 insert 할 때와 같은 값이 저장된다.

<insert

  useGeneratedKeys="true"  // 자동 생성된 키를 사용할거면 true 이다.

  keyProperty="no"  // 객체에 저장할때 프로퍼티 명은 no 이다.

  keyColumn="member_id"  // DBMS 에 저장된 컬럼 명은 member_id 이다.

<?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="MemberMapper">

  <insert id="insert" parameterType="bitcamp.myapp.vo.Member" 
          useGeneratedKeys="true" keyProperty="no" keyColumn="member_id">
    insert into app_member(name, email, pwd, tel)
    values(#{name}, #{email}, sha2(#{password},256), #{tel})
  </insert>

  <select id="findAll" resultType="bitcamp.myapp.vo.Member">
    select 
      member_id as no, 
      name, 
      email, 
      created_date as createdDate 
    from 
      app_member
    order by 
      member_id desc
  </select>

  <select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Member">
    select 
      member_id as no, 
      name, 
      email, 
      tel,
      created_date as createdDate 
    from 
      app_member
    where 
      member_id=#{no}
  </select>
  
  <update id="update" parameterType="bitcamp.myapp.vo.Member">
    update app_member set
      name=#{name}, 
      email=#{email}, 
      pwd=sha2(#{password},256), 
      tel=#{tel}
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_member
    where member_id=#{no}
  </delete>
</mapper>
<?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="StudentMapper">

  <insert id="insert" parameterType="bitcamp.myapp.vo.Student" >
    insert into app_student(
      member_id,
      pst_no,
      bas_addr,
      det_addr,
      work,
      gender,
      level)
    values(#{no},#{postNo},#{basicAddress},#{detailAddress},#{working},#{gender},#{level})
  </insert>

  <select id="findAll" resultType="bitcamp.myapp.vo.Student">
    select
      m.member_id as no,
      m.name,
      m.email,
      m.tel,
      s.work as working,
      s.level
    from 
      app_student s
      inner join app_member m on s.member_id = m.member_id
    order by
      m.name asc
  </select>

  <select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Student">
    select
      m.member_id as no,
      m.name,
      m.email,
      m.tel,
      m.created_date as createdDate,
      s.pst_no as postNo,
      s.bas_addr as basicAddress,
      s.det_addr as detailAddress,
      s.work as working,
      s.gender,
      s.level
    from 
      app_student s
      inner join app_member m on s.member_id = m.member_id
    where 
      m.member_id=#{no}
  </select>
  
  <select id="findByKeyword" parameterType="string" resultType="bitcamp.myapp.vo.Student">
    select
      m.member_id as no,
      m.name,
      m.email,
      m.tel,
      s.work as working,
      s.level
    from 
      app_student s
      inner join app_member m on s.member_id = m.member_id
    where
      m.name like(concat('%', #{keyword}, '%'))
      or m.email like(concat('%', #{keyword}, '%'))
      or m.tel like(concat('%', #{keyword}, '%'))
      or s.bas_addr like(concat('%', #{keyword}, '%'))
      or s.det_addr like(concat('%', #{keyword}, '%'))
    order by
      m.name asc
  </select>
  
  <update id="update" parameterType="bitcamp.myapp.vo.Student">
    update app_student set
      pst_no=#{postNo},
      bas_addr=#{basicAddress},
      det_addr=#{detailAddress},
      work=#{working},
      gender=#{gender},
      level=#{level} 
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_student
    where member_id=#{no}
  </delete>
</mapper>
<?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="TeacherMapper">

  <insert id="insert" parameterType="bitcamp.myapp.vo.Teacher" >
    insert into app_teacher(
      member_id,
      degree,
      school,
      major,
      wage)
    values(#{no},#{degree},#{school},#{major},#{wage})
  </insert>

  <select id="findAll" resultType="bitcamp.myapp.vo.Teacher">
    select
      m.member_id as no,
      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
  </select>

  <select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Teacher">
    select
      m.member_id as no,
      m.name,
      m.email,
      m.tel,
      m.created_date as createdDate,
      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}
  </select>
  
  <update id="update" parameterType="bitcamp.myapp.vo.Teacher">
    update app_teacher set
      degree=#{degree},
      school=#{school},
      major=#{major},
      wage=#{wage}
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_teacher
    where member_id=#{no}
  </delete>
</mapper>

 

mybatis-config.xml 에 driver 정보 및 url, username, password 입력한다.

<mappers> 속성에 Mapper 파일 지정한다.

<?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>
  </environments>
  
  <mappers>
    <mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
  </mappers>
  
</configuration>

 

SqlSessionFactoryBuilder 객체로 SqlSessionFactory 객체 만들고 Proxy 패턴 이용하여 BitcampSqlSessionFactory 생성자 매개변수에 전달한다.

이를 TransactionManager 객체 생성시 매개변수로 전달한다.

----DaoImpl 생성시 매개변수로 sqlSessionFactory 전달한다.

public class ServerApp {

/* 생략 */

  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");

    // 4) SqlSessionFactoryBuilder 객체 준비
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

    // 5) builder를 이용하여 SqlSessionFactory 객체 생성
    // 6) 오리지널 SqlSessionFactory에 트랜잭션 보조 기능이 덧붙여진 프록시 객체를 준비한다.
    BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
        builder.build(mybatisConfigInputStream));

    // 7) BitcampSqlSessionFactory객체를 이용하여 트랜잭션을 다루는 객체를 준비한다.
    TransactionManager txManager = new TransactionManager(sqlSessionFactory);

    BoardDaoImpl boardDao = new BoardDaoImpl(sqlSessionFactory);
    MemberDaoImpl memberDao = new MemberDaoImpl(sqlSessionFactory);
    StudentDaoImpl studentDao = new StudentDaoImpl(sqlSessionFactory);
    TeacherDaoImpl teacherDao = new TeacherDaoImpl(sqlSessionFactory);

    this.studentHandler = new StudentHandler("학생", txManager, memberDao, studentDao);
    this.teacherHandler = new TeacherHandler("강사", txManager, memberDao, teacherDao);
    this.boardHandler = new BoardHandler("게시판", boardDao);
  }

 

TransactionManager 클래스에 startTransaction(), commi(), rollback() 메서드 정의한다.

package bitcamp.util;

import org.apache.ibatis.session.SqlSession;

public class TransactionManager {

  BitcampSqlSessionFactory sqlSessionFactory;

  public TransactionManager(BitcampSqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  public void startTransaction() {
    sqlSessionFactory.prepareSqlSessionForThread();
  }

  public void commit() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    sqlSession.commit();
    sqlSessionFactory.clearSqlSessionForThread();
  }

  public void rollback() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    sqlSession.rollback();
    sqlSessionFactory.clearSqlSessionForThread();
  }
}

 

BitcampSqlSessionFactory 클래스 정의한다.

생성자 매개변수로 받은 SqlSessionFactory 를 original 에 저장한다.

ThreadLocal 을 생성하고 sqlSessionLocal 에 저장한다.

prepareSqlSessionForThread() 실행시 sqlSessionLocal 에서 sqlSession 을 가져오는데, null 이면 sqlSession 을 새로 생성(autoCommit = false) 해서 sqlSessionLocal 에 저장한다.

clearSqlSessionForThread() 실행시 original 을 close 한다.

openSession() 실행시 sqlSessionLocal 에서 sqlSession 가져오는데, null 이면 트랙잭션 없이 사용할 sqlSession 을 리턴한다.

package bitcamp.util;

import java.sql.Connection;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;

public class BitcampSqlSessionFactory implements SqlSessionFactory {

  SqlSessionFactory original;

  ThreadLocal<SqlSession> sqlSessionLocal = new ThreadLocal<>();

  public void prepareSqlSessionForThread() {
    SqlSession sqlSession = sqlSessionLocal.get();
    if (sqlSession == null) {
      sqlSessionLocal.set(new BitcampSqlSession(original.openSession(false)));
    }
  }

  public void clearSqlSessionForThread() {
    BitcampSqlSession sqlSession = (BitcampSqlSession) sqlSessionLocal.get();
    sqlSession.closeOriginal();
    sqlSessionLocal.set(null);
  }

  public BitcampSqlSessionFactory(SqlSessionFactory original) {
    this.original = original;
  }

  @Override
  public SqlSession openSession() {
    SqlSession sqlSession = sqlSessionLocal.get();
    if (sqlSession == null) {
      // 트랜잭션 없이 사용할 SqlSession 리턴
      return original.openSession(true);
    }

    // 스레드에서 공유할 SqlSession 객체 리턴
    return sqlSession;
  }

  @Override
  public SqlSession openSession(boolean autoCommit) {
    return original.openSession(autoCommit);
  }

  @Override
  public SqlSession openSession(Connection connection) {
    return original.openSession(connection);
  }

  @Override
  public SqlSession openSession(TransactionIsolationLevel level) {
    return original.openSession(level);
  }

  @Override
  public SqlSession openSession(ExecutorType execType) {
    return original.openSession(execType);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
    return original.openSession(execType, autoCommit);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
    return original.openSession(execType, level);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, Connection connection) {
    return original.openSession(execType, connection);
  }

  @Override
  public Configuration getConfiguration() {
    return original.getConfiguration();
  }
}

 

Proxy 패턴 사용하여 트랜잭션에서 예컨대 memberDao.insert(s) 이후 sqlSession 이 자동 close 되지 않도록 BitcampSqlSession 으로 감싸서 close() 본문 주석 처리하고, 추후 closeOriginal() 호출해서 close 되도록 한다.

commit(), rollback() 시 close() 는 자동으로 되는 것으로 추정된다.

Proxy 패턴 사용으로 모든 메서드 호출시 original 메서드에 위임하는 코드가 있다.

package bitcamp.util;

import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;

public class BitcampSqlSession implements SqlSession {

  SqlSession original;

  public void closeOriginal() {
    original.close();
  }

  public BitcampSqlSession(SqlSession original) {
    this.original = original;
  }

  @Override
  public <T> T selectOne(String statement) {
    return original.selectOne(statement);
  }

  @Override
  public <T> T selectOne(String statement, Object parameter) {
    return original.selectOne(statement, parameter);
  }

  @Override
  public <E> List<E> selectList(String statement) {
    return original.selectList(statement);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    return original.selectList(statement, parameter);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    return original.selectList(statement, parameter, rowBounds);
  }

  @Override
  public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
    return original.selectMap(statement, mapKey);
  }

  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
    return original.selectMap(statement, parameter, mapKey);
  }

  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey,
      RowBounds rowBounds) {
    return original.selectMap(statement, parameter, mapKey, rowBounds);
  }

  @Override
  public <T> Cursor<T> selectCursor(String statement) {
    return original.selectCursor(statement);
  }

  @Override
  public <T> Cursor<T> selectCursor(String statement, Object parameter) {
    return original.selectCursor(statement, parameter);
  }

  @Override
  public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
    return original.selectCursor(statement, parameter, rowBounds);
  }

  @Override
  public void select(String statement, Object parameter, ResultHandler handler) {
    original.select(statement, parameter, handler);
  }

  @Override
  public void select(String statement, ResultHandler handler) {
    original.select(statement, handler);
  }

  @Override
  public void select(String statement, Object parameter, RowBounds rowBounds,
      ResultHandler handler) {
    original.select(statement, parameter, rowBounds, handler);
  }

  @Override
  public int insert(String statement) {
    return original.insert(statement);
  }

  @Override
  public int insert(String statement, Object parameter) {
    return original.insert(statement, parameter);
  }

  @Override
  public int update(String statement) {
    return original.update(statement);
  }

  @Override
  public int update(String statement, Object parameter) {
    return original.update(statement, parameter);
  }

  @Override
  public int delete(String statement) {
    return original.delete(statement);
  }

  @Override
  public int delete(String statement, Object parameter) {
    return original.delete(statement, parameter);
  }

  @Override
  public void commit() {
    original.commit();
  }

  @Override
  public void commit(boolean force) {
    original.commit(force);
  }

  @Override
  public void rollback() {
    original.rollback();
  }

  @Override
  public void rollback(boolean force) {
    original.rollback(force);
  }

  @Override
  public List<BatchResult> flushStatements() {
    return original.flushStatements();
  }

  @Override
  public void close() {
    //original.close();
  }

  @Override
  public void clearCache() {
    original.clearCache();
  }

  @Override
  public Configuration getConfiguration() {
    return original.getConfiguration();
  }

  @Override
  public <T> T getMapper(Class<T> type) {
    return original.getMapper(type);
  }

  @Override
  public Connection getConnection() {
    return original.getConnection();
  }
}

 

 

 

DAO 구현체 자동 생성하기

 

① 이전 방식

《interface》BoardDao 구현한 BoardDaoImpl 생성한다. ← 개발자가 인터페이스 규격에 맞춰 직접 구현 클래스를 정의

 

② 자동화 방식

DaoGenerator 에서 ① getObject(BoardDao.class) 한다. 여기서 BoardDao 구현체 리턴 한다.

DaoGenerator 에서 클래스 생성에 필요한 정보를 Proxy 로 주면 인터페이스 구현체를 리턴 받는다.

BoardDao 구현체의 ① 메서드 호출 하면 여기에서 포함하고 있는 InvocationHandler 구현체에게 ② invoke (메서드 정보) 한다. ③ 리턴 받아서 ④ 전달 한다.

 

 

 

43. Mybatis SQL 매퍼 사용하기 II(DAO 구현 자동화 포함) 

 

### 43. Mybatis SQL 매퍼 사용하기 II(DAO 구현 자동화 포함) 
- 도메인 클래스에 별명을 부여하고 사용하는 방법 
- 컬럼 이름과 객체의 프로퍼티 이름을 미리 연결해 두는 방법 
- JDBC 및 DBMS 정보를 .properties 파일로 분리하는 방법 
- DAO 인터페이스 구현체를 자동으로 생성하는 방법  
- Mybatis 의 설정을 이용하여 DAO 구현체를 자동으로 생성하는 방법

 

mybatis-config.xml 에서 주소 지정하지 않고 jdbc.properties 파일에 저장하는 방법이다.

<typeAliases>

  <package name=" "> 의 name에 패키지 경로 지정하면 ----Mapper.xml 파일에서 parameterType=" " 지정할때 경로 다 지정하지 않아도 된다.

<properties resource=" "> 에 jdbc.properties 파일 경로 입력하면 <property value=" "> 의 value 에 jdbc.properties 파일에 저장된 변수를 지정해 그 값을 사용한다.

<?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>

  <properties resource="bitcamp/myapp/config/jdbc.properties"></properties>

  <typeAliases>
    <!-- 지정된 패키지의 모든 클래스에 대해 클래스 이름과 같은 이름으로 별명을 부여한다. -->
    <package name="bitcamp.myapp.vo"/>
  </typeAliases>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
      </dataSource>
    </environment>
  </environments>
  
  <mappers>
    <mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
    <mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
  </mappers>
  
</configuration>

 

# jdbc.properties
# name=value
jdbc.driver=org.mariadb.jdbc.Driver
jdbc.url=jdbc:mariadb://localhost:3306/studydb
jdbc.username=study
jdbc.password=1111

 

BoardMapper.xml 에서 ResultMap 사용해서 SQL 문에서 as 를 사용하지 않도록 하는 방법이다.

type 에 class 명, id 에 <select> 속성에서 사용할 이름을 지정한다.

pk 는 <id> 로 지정하고 나머지는 <result> 로 한다.

속성에서 column=" " 에 컬럼명 적고, property=" " 에 프로퍼티명 적는다.

<select resultMap=" "> 에서 위에서 지정한 id 를 적는다.

<?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">

  <resultMap type="board" id="boardMap">
    <id column="board_id" property="no"/>
    <result column="title" property="title"/>
    <result column="content" property="content"/>
    <result column="pwd" property="password"/>
    <result column="created_date" property="createdDate"/>
    <result column="view_cnt" property="viewCount"/>
  </resultMap>

  <insert id="insert" parameterType="board">
    insert into app_board(title, content, pwd) 
    values(#{title}, #{content}, #{password})
  </insert>

  <select id="findAll" resultMap="boardMap">
    select 
      board_id, 
      title, 
      created_date, 
      view_cnt 
    from 
      app_board 
    order by 
      board_id desc
  </select>

  <select id="findByNo" parameterType="int" resultMap="boardMap">
    select 
      board_id, 
      title, 
      content, 
      pwd, 
      created_date, 
      view_cnt
    from 
      app_board 
    where 
      board_id=#{no}
  </select>
  
  <update id="increaseViewCount" parameterType="int">
    update app_board set
      view_cnt = view_cnt + 1
    where board_id=#{maumdaerohaedodoi}
  </update>
  
  <select id="findByKeyword" parameterType="string" resultMap="boardMap">
    select 
      board_id, 
      title, 
      created_date, 
      view_cnt
    from 
      app_board 
    where 
      title like(concat('%',#{keyword},'%'))
      or content like(concat('%',#{keyword},'%'))
    order by 
      board_id desc
  </select>
  
  <update id="update" parameterType="board">
    update app_board set 
      title=#{title}, 
      content=#{content} 
    where board_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_board 
    where board_id=#{no}
  </delete>
</mapper>
<?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="MemberMapper">

  <resultMap type="member" id="memberMap">
    <id column="member_id" property="no"/>
    <result column="name" property="name"/>
    <result column="email" property="email"/>
    <result column="tel" property="tel"/>
    <result column="created_date" property="createdDate"/>
  </resultMap>

  <insert id="insert" parameterType="member" 
          useGeneratedKeys="true" keyProperty="no" keyColumn="member_id">
    insert into app_member(name, email, pwd, tel)
    values(#{name}, #{email}, sha2(#{password},256), #{tel})
  </insert>

  <select id="findAll" resultMap="memberMap">
    select 
      member_id, 
      name, 
      email, 
      created_date 
    from 
      app_member
    order by 
      member_id desc
  </select>

  <select id="findByNo" parameterType="int" resultMap="memberMap">
    select 
      member_id, 
      name, 
      email, 
      tel,
      created_date 
    from 
      app_member
    where 
      member_id=#{no}
  </select>
  
  <update id="update" parameterType="member">
    update app_member set
      name=#{name}, 
      email=#{email}, 
      pwd=sha2(#{password},256), 
      tel=#{tel}
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_member
    where member_id=#{no}
  </delete>
</mapper>
<?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="StudentMapper">

  <resultMap type="student" id="studentMap">
    <id column="member_id" property="no"/>
    <result column="name" property="name"/>
    <result column="email" property="email"/>
    <result column="tel" property="tel"/>
    <result column="created_date" property="createdDate"/>
    <result column="pst_no" property="postNo"/>
    <result column="bas_addr" property="basicAddress"/>
    <result column="det_addr" property="detailAddress"/>
    <result column="work" property="working"/>
    <result column="gender" property="gender"/>
    <result column="level" property="level"/>
  </resultMap>

  <insert id="insert" parameterType="student" >
    insert into app_student(
      member_id,
      pst_no,
      bas_addr,
      det_addr,
      work,
      gender,
      level)
    values(#{no},#{postNo},#{basicAddress},#{detailAddress},#{working},#{gender},#{level})
  </insert>

  <select id="findAll" resultMap="studentMap">
    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
  </select>

  <select id="findByNo" parameterType="int" resultMap="studentMap">
    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 
      m.member_id=#{no}
  </select>
  
  <select id="findByKeyword" parameterType="string" resultMap="studentMap">
    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(concat('%', #{keyword}, '%'))
      or m.email like(concat('%', #{keyword}, '%'))
      or m.tel like(concat('%', #{keyword}, '%'))
      or s.bas_addr like(concat('%', #{keyword}, '%'))
      or s.det_addr like(concat('%', #{keyword}, '%'))
    order by
      m.name asc
  </select>
  
  <update id="update" parameterType="student">
    update app_student set
      pst_no=#{postNo},
      bas_addr=#{basicAddress},
      det_addr=#{detailAddress},
      work=#{working},
      gender=#{gender},
      level=#{level} 
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_student
    where member_id=#{no}
  </delete>
</mapper>
<?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="TeacherMapper">

  <resultMap type="teacher" id="teacherMap">
    <id column="member_id" property="no"/>
    <result column="name" property="name"/>
    <result column="email" property="email"/>
    <result column="tel" property="tel"/>
    <result column="created_date" property="createdDate"/>
    <result column="degree" property="degree"/>
    <result column="school" property="school"/>
    <result column="major" property="major"/>
    <result column="wage" property="wage"/>
  </resultMap>
  
  <insert id="insert" parameterType="teacher" >
    insert into app_teacher(
      member_id,
      degree,
      school,
      major,
      wage)
    values(#{no},#{degree},#{school},#{major},#{wage})
  </insert>

  <select id="findAll" resultMap="teacherMap">
    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
  </select>

  <select id="findByNo" parameterType="int" resultMap="teacherMap">
    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}
  </select>
  
  <update id="update" parameterType="teacher">
    update app_teacher set
      degree=#{degree},
      school=#{school},
      major=#{major},
      wage=#{wage}
    where member_id=#{no}
  </update>
  
  <delete id="delete" parameterType="int">
    delete from app_teacher
    where member_id=#{no}
  </delete>
</mapper>

 

DaoGenerator 로 Dao 생성할 수 있다.

BoardDaoImpl, MemberDaoImpl, StudentDaoImpl, TeacherDaoImpl 는 sql 만 일부 다르고 같은 형식이라 자동 생성하도록 설정 가능하다.

package bitcamp.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.apache.ibatis.session.SqlSessionFactory;
import bitcamp.myapp.dao.BoardDao;

public class DaoGenerator {

  SqlSessionFactory sqlSessionFactory;

  public DaoGenerator(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
  }

  @SuppressWarnings("unchecked")
  public <T> T getObject(Class<T> classInfo) {
    String className = classInfo.getName();

    return (T) Proxy.newProxyInstance(
        getClass().getClassLoader(), // 현재 클래스의 로딩을 담당한 관리자: 즉 클래스 로딩 관리자
        new Class[] {classInfo}, // 클래스가 구현해야 할 인터페이스 정보 목록
        new MyInvocationHandler() // InvocationHandler 객체
        );
  }

  // 자동 생성된 프록시 객체에 대해 메서드를 호출하면
  // 실제 InvocationHandler의 invoke()가 호출된다.
  //
  class MyInvocationHandler implements InvocationHandler {

    // 프록시 객체에 대해 메서드를 호출하면 이 메서드가 실행된다.
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.printf("%s() 메서드 호출했음!\n", method.getName());
      if (method.getReturnType() == int.class) {
        return 1;
      }
      return null;
    }
  }


  public static void main(String[] args) {
    DaoGenerator generator = new DaoGenerator(null);
    BoardDao dao = generator.getObject(BoardDao.class);

    dao.insert(null);
    dao.findAll();
    dao.findByNo(1);
    dao.update(null);
    dao.delete(1);
  }

}

 

 

 


 

조언

 

*

 

 


 

과제

 

/