본문 바로가기

국비 교육

2020.11.9 일자 수업 : Chain of Responsibility, MyBatis, Persistence Framework

 실습 - Chain of Responsibility 

 

CommandFilterManager의 내부적인 작동 구조를 LinkedList와 비슷하게 할 수도 있다. CommandFilterManager는 LinkedList가 Node를 관리하듯이 firstChain과 lastChain을 갖는다. Chain은 LinkedList의 Node와 같다. Chain은 수행할 작업을 담은 filter와 다음 Chain을 가리키는 nextChain을 갖는다. 어떤 Chain에 대해서 doFilter를 호출하면 해당 Chain 안에 담긴 filter에 대해서 doFilter를 호출한다. 그리고 각 Filter는 doFilter 메서드 몸체에서 작업을 수행한 후 두 번째 파라미터로 받은 nextChain에 대하여 doFilter를 호출할 것이다.

 

CommandFilterManager에서 doFilter를 실행하면 firstChain에 대하여 doFilter가 호출되고, 작업을 실행하는 도중에 다음 Chain에 대하여 doFilter가 호출되는 방식으로 첫 Chain 부터 마지막 Chain까지 호출될 것이다. 가장 마지막 Chain은 doFilter가 호출되어 filter가 작업을 실행하는 도중에 다음 Chain에 대하여 호출을 하지 않아야만 doFilter의 호출이 끝날 수 있다.

 

또한 CommandFilterManager의 init과 destroy도 firstChain에서부터 마지막 Chain까지 순서대로 각 filter에 대하여 init/destroy 메서드를 호출한다.

public class CommandFilterManager {
  Chain firstChain;
  Chain lastChain;
  
  public void add(CommandFilter filter) {
    Chain chain = new Chain(filter);
    if (lastChain == null) {
      firstChain = lastChain = chain;
      return;
    }
    lastChain.nextChain = chain;
    lastChain = chain;
  }
  
  public FilterChain getFilterChains() {
    return firstChain;
  }
  
  public void init(Map<String, Object> context) throws Exception {
    Chain chain = firstChain;
    while (chain != null) {
      chain.filter.init(context);
      chain = chain.nextChain;
    }
  }
  
  public void destroy() {
    Chain chain = firstChain;
    while (chain != null) {
      chain.filter.destroy();
      chain = chain.nextChain;
    }
  }
  
  public void doFilter(Request request) throws Exception {
    firstChain.doFilter(request);
  }
  
  private static class Chain implements FilterChain {
    CommandFilter filter;
    Chain nextChain;
    
    public Chain(CommandFilter filter) {
      this.filter = filter;
    }
    
    @Override
    public void doFilter(Request request) throws Exception {
      filter.doFilter(request, nextChain);
    }
  }
}

이렇게 되면 각 Chain이 담는 CommandFilter 구현체들의 doFilter 메서드에서 이미 수행할 작업을 모두 수행한 후에 마지막으로 nextChain에 대하여 doFilter를 호출하고 있기 때문에 저번 실습의 코드를 수정하지 않아도 된다.

 

마찬가지로, App에서도 CommandFilterManager에 대하여 doFilter를 호출하고 있으므로 수정할 필요가 없다.


 퍼시스턴스 프레임워크 

 

원래는 자바 소스 코드가 직접 JDBC API를 호출하고 JDBC Driver가 API에 맞춰 DBMS와 통신했다. 그러나 이제는 자바로 직접 JDBC API를 호출하는 코드를 작성하지 않고 퍼시스턴스 프레임워크가 JDBC API를 대신 호출하게 함으로써 자바 코드는 DBMS와 관련된 기능을 완전히 숨길 수가 있다. 이것도 캡슐화의 일종으로 JDBC 프로그래밍을 캡슐화한 것이라고 할 수 있다. 퍼시스턴스 프레임워크는 다음과 같이 두 종류가 있다.

 

SQL Mapper

DBMS의 데이터들에 접근하는 동시에 SQL Mapper는 SQL문과 자바 객체의 필드를 매핑해주어 데이터를 객체로써 편리하게 다룰 수 있게 한다. 사용자는 (.xml 파일에서) SQL문을 명시하여 SQL Mapper를 호출하면, SQL Mapper는 이 SQL문을 JDBC API를 이용하여 가공하여 DBMS에게 전송한다.

 

SQL문을 명시하기 때문에 특정 DBMS의 종류에 종속되는 단점이 있는 반면, DBMS에 최적화를 시켜 DBMS의 성능을 최대로 끌어올릴 수 있다 (SQL을 잘 다루는 개발자 한정)는 장점이 있다. 이 프레임워크의 예시로는 MyBatis가 있다.

 

실무에서 사용되는, 특히 SI 쪽에서 많이 사용되는 관공서 프레임워크는 egovframework으로, 세계적으로 많이 사용되는 프레임워크를 현지화시킨 오픈소스 프레임워크이다. 이 중에는 Persistent Layer가 있고, 여기서 MyBatis가 사용된다.

 

JDBC API는 SQL Mapper로 분리되고, SQL문은 .xml 파일로 분리된다.

OR Mapper (Objection Relational Mapper)

OR Mapper는 SQL Mapper와 달리 사용자가 SQL이 아니라 전용 질의어를 통해 OR Mapper을 호출하고, 이를 통해 객체들 간의 관계를 포함한 객체 자체가 데이터로 다뤄진다. 사용자가 전용 질의어를 이용하여 OR Mapper를 호출하면 OR Mapper는 이 전용 질의어에 대하여 DBMS 전용 Adapter에서 가공된 SQL문을 받고, 이것을 JDBC API를 통해 가공하여 DBMS에게 보내주는 방식으로 작동한다. (사용하는 Adapter에 따라 이 시퀀스가 달라질 수 있다.) 

 

SQL문을 명시하지 않기 때문에 DBMS에 종속되지 않으나 각각의 DBMS에 최적화될 수는 없고, DBMS용 Adapter가 필요하다는 단점이 있다. 또한 OR Mapper도 종류가 여러가지이므로 코드가 특정 OR Mapper에게 종속된다는 단점이 있다. 

주로 서비스 업체에서 OR Mapper를 사용한다. 


 

MyBatis 시작하기

우리는 퍼시스턴스 프레임워크 중에서도 SQL Mapper인 MyBatis를 사용할 것이다. 일단 MyBatis가 사용할 jdbc에 관한 정보를 jdbc.properties에 저장한다.

# jdbc.properties
# key=value for application
jdbc.driver=com.mariadb.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/studydb
jdbc.username=study
jdbc.password=1111

.properties 확장자

응용 프로그램의 구성 가능한 파라미터들을 저장하기 위해 자바 관련 기술을 사용하는 파일들을 위한 확장자이다. 각 파라미터는 문자열들의 일부로 저장되는데, 한 문자열은 파라미터의 이름(키)를 저장하며, 다른 하나는 값을 저장한다. 

출처 : ko.wikipedia.org/wiki/.properties


maven.org에서 mybatis 라이브러리를 임포트할 수 있는 코드를 복사하여 build.gradle의 dependencies에 붙이고 콘솔에서 해당 프로젝트에 대해 gradle eclipse를 실행하면 간단히 mybatis를 임포트할 수 있다.

 

MyBatis를 시작하는 방법은 MyBatis 홈페이지에 자세하게 설명되어있으니 참고할 수가 있다.

 

일단 MyBatis의 주기능을 담당하는 SqlSessionFactory를 생성할 때 사용되는 SqlSessionFactory 설계도가 필요하다. 설계도로서 가장 핵심적인 역할을 하는 파일이 mybatis-config.xml 파일이다. 이 파일의 기본적인 틀은 MyBatis 홈페이지에서 제공하고 있다.\

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <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>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

 

이 파일을 수정함으로써 프로젝트에서 필요한 SqlSessionFactory를 생성할 수가 있다. 수정 내용은 다음과 같다.

  • configuration 태그 안에 가장 첫째 줄에 MyBatis가 사용할 JDBC에 관한 정보를 담은 jdbc.properties 파일을 지정한다. properties라는 태그에 resource라는 속성값으로 패키지 경로를 명시하면 된다.
  • 데이터베이스의 연결 정보를 지정하는 dataSource 태그 안에는 키와 값들이 나열되어있는데 여기서 값들을 jdbc.properties 파일에 지정된 key 값으로 수정해준다.
  • MyBatis가 실행할 SQL문을 정리한 .xml 파일을 Mapper 파일이라고 한다. 이 파일을 mappers 태그 안에 지정해줄 수 있다. mapper 태그 안에 resource라는 속성값으로 패키지 경로를 명시하면 된다.

수정 사항 이외에 각 태그가 하는 일은 아래 예제의 주석을 확인할 수 있다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <!-- 설정 파일에서 사용할 .properties 파일 정보 -->
  <properties resource="com/eomcs/pms/conf/jdbc.properties"></properties>

  <!-- DBMS 접속 정보들 -->
  <environments default="development">
  
    <!-- 한 개의 DBMS 접속 정보 -->
    <environment id="development">
    
      <!-- mybatis 가 트랜잭션을 다룰 때 사용할 방법을 지정 => JDBC API 사용 -->
      <transactionManager type="JDBC"/>
      
      <!-- 데이터베이스의 연결 정보 => jdbc.properties 파일에 설정된 key-value -->
      <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>
  
  <!-- SQL 문이 들어 있는 파일들 -->
  <mappers>
    <mapper resource="com/eomcs/pms/mapper/BoardMapper.xml"/>
  </mappers>
</configuration>

Select SQL 문 전송하기

이제 BoardMapper.xml 파일을 만들고 그 안에 MyBatis 홈페이지에서 제공하는 틀을 집어넣는다. 틀은 다음과 같다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

이제 BoardDao에 정의된 각 메서드에서 직접 JDBC를 사용하여 데이터베이스에 SQL을 전송하고 이 과정에서 자바 객체를 다루는 작업을 MyBatis가 할 수 있도록 이 Mapper 파일을 수정해야 한다. 수정 내용은 다음과 같다.

  • mapper 태그의 namespace 속성은 이 파일에서 작성되는 SQL문을 담는 그룹명이다. 보통 이 SQL문을 전송하는 클래스명으로 지정한다. 대부분 이 SQL문을 전송하는 클래스는 DAO 클래스이므로 아마도 DAO 인터페이스 혹은 클래스명이 될 것이다. 
  • select 태그는 select SQL 문을 정의하는 태그이다.
    • id 속성은 mybatis에서 SQL문을 검색할 때 사용되는 이름이다. 보통은 이 SQL을 사용하는 메서드 이름으로 지정한다.
    • resultType 속성은 select 결과물에서 각 레코드의 컬럼 값을 필드에 담을 클래스명이다. 만약 select의 결과물이 여러개의 레코드 값이라면 각 레코드 값을 담은 객체들을 List 컬렉션에 담아 리턴한다. 
  • select 태그 안에는 전송할 SQL문을 적으면 된다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<mapper namespace="BoardDao">
  <select id="findAll" resultType="com.eomcs.pms.domain.Board">
    select 
      b.no, 
      b.title, 
      b.cdt, 
      b.vw_cnt, 
      m.no writer_no, 
      m.name
    from 
      pms_board b 
      inner join pms_member m on b.writer=m.no
    order by 
      b.no desc
  </select>
</mapper>

이제 MyBatis를 사용할 준비가 되었으니, 원래 SQL문을 직접 전송하던 코드로 가서, MyBatis를 사용해볼 수가 있다. MyBatis 사용 방법은 다음과 같다. 

  • mybatis-config.xml 파일을 읽어들일 입력 스트림을 준비해야한다. 우리가 평소에 하던대로 FileReader나 InpuStream을 연결할 수가 있지만, 이 경우에는 우리가 직접 절대 경로를 지정해야줘야 하므로 경로가 굉장히 길어진다. 또한 프로젝트를 무엇으로 빌드했느냐에 따라 경로가 달라질 수 있다. 따라서 Resource 클래스에 getResourceAsStream 메서드를 호출하면 JVM이 대신 resource 패키지가 있는 곳을 찾아준다. 이것을 이용하면, 바로 resource 패키지 안의 경로만 지정해도 JVM이 알아서 패키지의 위치를 찾아준다.
  • SqlSessionFactoryBuilder에게 이 inputStream를 파라미터로 주고 build를 호출하면 SqlSessionFactoryBuilder가 mybatis-config.xml 파일에 따라 SqlSessionFactory를 생성하여 리턴한다.
  • 이 SqlSessionFactory에게 openSession 메서드를 호출하면, SqlSessionFactory는 SqlSession 객체를 생성하여 리턴한다.
  • SqlSession 객체에 대하여 selectList 메서드를 호출하고, "Mapper이름.SQL ID"를 문자열 형태로 파라미터로 넘겨주면 해당 Mapper 파일을 찾고 그 안에 id가 일치하는 SQL문을 찾아 DBMS에게 전송한다. 그리고 그 결과를 여러개의 Board 객체에 담고, 그것을 List로 묶어 리턴한다.
 @Override
  public List<Board> findAll() throws Exception {
    InputStream inputStream = Resources.getResourceAsStream(
        "com/eomcs/pms/conf/mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory =
        new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("BoardDao.findAll");
    }
  }

그렇다면 SQlSession은 select 결과로 받은 레코드 값들을 어떻게 Board 객체에 저장할까?

SQLSession은 자바 객체에 레코드 정보를 담을 때 각 컬럼명과 자바 프로퍼티명이 같은 곳에 값을 저장한다. 즉, set+컬럼명(가장 앞 글자 대문자로 변환)을 호출하여 필드 값을 변경한다. 따라서 클래스의 필드를 찾아 넣는 것이 아니라, 세터 메서드로 변환했을 때 해당 메서드가 그 클래스에 정의되어있다면 그 메서드를 호출하여 값을 바꾸는 것이다. (MyBatis의 최신 버전은 일치하는 프로퍼티가 없을시 컬럼명과 일치하는 필드를 검색한다. 아예 일치하는 프로퍼티도도 없을시에는 들어가지 않는다.)


프로퍼티(Property)란?

다른 말로는 게터/세터 메서드이고, 정확히는 게터/세터 메서드는 read/write property, 게터 메서드는 read only property이다. 프로퍼티명은 get/set 뒤에오는 단어에서 가장 첫 글자를 대문자에서 소문자로 바꾼 단어를 말한다.


프로퍼티명과 컬럼명이 일치하지 않는 컬럼 값들은 버려진다. 이 문제를 해결하기 위한 두 가지 방법이 있다. 

  • Mapper 파일에서 작성된 select SQL문에 명시된 컬럼명들에 대해 자바 객체 프로퍼티명과 일치하는 별명을 붙인다. 이렇게 하면 SqlSession은 이 별명과 프로퍼티명을 비교하여 일치하면 값을 변경한다.
	<select id="findAll" resultMap="BoardMap">
		select
		b.no,
		b.title,
		b.cdt as RegisteredDate,
		b.vw_cnt as viewCount,
		m.no writer_no,
		m.name
		from
		pms_board b
		inner join pms_member m on b.writer=m.no
		order by
		b.no desc
	</select>
  • Mapper 파일에서 직접 컬럼명과 필드를 연결해주는 코드를 짜줌으로써 해결할 수가 있다. 특히 Board 객체의 필드 중에는 Member 타입의 필드인 Writer도 있기 때문에 이 코드가 필수적이다. 이 코드는 다음과 같이 작성한다.
    • resultMap 태그를 작성하고, type 속성으로 각 컬럼값을 연결할 필드를 가진 클래스명을 적는다. id 속성은 이 연결 정보를 가리키는 이름이며 SQL문을 정의할 때 사용된다.
    • resultMap 태그 안에는 result 태그를 작성하고 column 속성에 컬럼명, 그리고 property 값에는 이 컬럼을 연결할 프로퍼티명을 적는다.
    • 컬럼들 중 pk가 되는 컬럼은 id 태그로 작성하면 이 객체를 보관하고, 나중에 이 id를 사용하게 될일이 있으면 보관된 그 객체를 그대로 담는다.
    • 자바 필드 중에는 Member 타입인 필드도 있는데, 이때는 association 태그를 사용하여 property 속성 값으로는 Board 클래스의 프로퍼티명, javaType 속성 값으로는 해당 필드의 클래스 타입명을 작성한다.
      • 이 Member 객체에 데이터 정보를 담는 코드는 마찬가지로 association 태그 안에 result 태그로 작성한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

	<resultMap type="com.eomcs.pms.domain.Board" id="BoardMap">
		<id column="no" property="no" />
		<result column="title" property="title" />
		<result column="cdt" property="registeredDate" />
		<result column="vw_cnt" property="viewCount" />
		
		<association property="writer" javaType="com.eomcs.pms.domain.Member">
			<result column="writer_no" property="no" />
			<result column="name" property="name" />
		</association>
	</resultMap>

	<select id="findAll" resultMap="BoardMap">
		select
		b.no,
		b.title,
		b.cdt,
		b.vw_cnt,
		m.no writer_no,
		m.name
		from
		pms_board b
		inner join pms_member m on b.writer=m.no
		order by
		b.no desc
	</select>
</mapper>

 


Insert SQL 문 전송하기

이제 BoardDao의 insert 메서드에 들어있는 insert SQL문을 직접 전송하는 코드를 MyBatis를 사용하는 코드로 변경해보자. 그러기 위해서는 Mapper 파일에 insert 태그를 추가해야 한다. Mapper 파일에 추가되는 내용은 다음과 같다.

  • insert SQL문을 정의하는 insert 태그를 작성하고, parameterType의 속성 값으로는 해당 SQL문을 실행할 때 넘겨주는 파라미터 타입을 지정한다.
  • insert 태그 안에는 BoardDao의 insert 메서드에서 전송하던 SQL문을 작성한다.
  • SQL문 안에서 values 다음의 괄호 안에 나열되는 #{} 안에는 Board의 프로퍼티명을 넣는다. 단, writer는 자바 클래스에서는 Member 타입으로 지정되나, 데이터베이스 안에서는 이 객체의 no 값이 저장되므로 이것을 간결하게 writer.no 처럼 프로퍼티명을 나열할 수가 있다. 
	<insert id="insert" parameterType="com.eomcs.pms.domain.Board">
	  insert into pms_board(title,content,writer) 
	  values(#{title},#{content},#{writer.no})
	</insert>

그리고 BoardDao에서 insert 메서드를 다음과 같이 수정한다. 이렇게 하면 BoardDao라는 이름의 Mapper 파일에 찾아가서 id 속성 값이 insert인 태그 사이에 있는 SQL문을 실행한다. 또한 두번째 파라미터로 넘긴 board 객체의 각 필드 값을 SQL문에 삽입한다.

  @Override
  public int insert(Board board) throws Exception {
    InputStream inputStream = Resources.getResourceAsStream(
        "com/eomcs/pms/conf/mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory =
        new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.insert("BoardDao.insert", board);
    }
  }

다만 myBatis의 기본 오토 커밋 설정은 false으로 설정되어있기 때문에 오토커밋을 true로 설정해야만 insert한 결과가 바로 데이터 베이스에 적용된다.

 

myBatis에서 오토 커밋을 직접 지정해주려면 SqlSessionFactory에 대하여 openSession 메서드를 호출할 때, 파라미터로 true, false를 넘겨주어 설정할 수가 있다. 따라서 다음과 같이 true를 openSession 메서드의 파라미터로 지정한다.

SqlSession sqlSession = sqlSessionFactory.openSession(true)

 

MyBatis는 sqlSession에 대하여 메서드를 호출할 때, insert, update, delete 메서드를 똑같은 ExcuteUpdate 메서드로 실행하기 때문에 그냥 파라미터로 들어간 정보대로 insert SQL을 DBMS에 전달한다. 따라서 다음 문장은 문제될 것이 없다. 하지만, 웬만하면 의미적인 통일을 위해 파라미터와 메서드를 같게 해주는 것이 좋다.

sqlSession.delete("BoardDao.insert", board);

OGNL (Object Graph Navigation Language)

객체가 객체를 포함하는 관계에서 이 관계를 간결하게 표현하는 방법이 있다. 원래는 게터 메서드를 나열하여 해당 필드를 조회할 수 있으나 이 OGNL 문법을 사용하면 간결히 프로퍼티명만을 나열하여 원하는 값을 조회할 수 있다. 예를 들면 다음과 같다.

b.getWriter().getAddr().getBaseAddr()

-> OGNL 표기법
b.writer.addr.baseAddr