MyBatis configuration 구성
mybatis의 config파일에서 DOCTYPE configuration으로 선언해주면 configuration, environments, enviroment가 필요하다.
<?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">
MyBatis configuration 각 기능들에 대한 문서 링크정리
- configuration
- properties
- settings
- typeAliases
- typeHandlers
- objectFactory
- plugins
- environments
- environment
- transactionManager
- dataSource
- Type=POOLED : 커넥션 풀링이 적용되어 있는 데이터베이스가 들어가게 된다.
- mybatis는 스프링과 다르게 hikariCP가 아닌 자체 커넥션 풀을 사용한다.(성능은 hikariCP가 우세)
- 실무랑 닿아 있는 커넥션 풀을 구성하고 싶으면 POOLED가 아닌 다른 것을 사용해주어야 한다.
- property에 값을 부여하게 되면 jdbc에서 만들어주었던 객체를 선언해줌을 알 수가 있다.
- Type=POOLED : 커넥션 풀링이 적용되어 있는 데이터베이스가 들어가게 된다.
- environment
- databaseIdProvider
- mappers
MyBatis-configration 문서의 예시
<?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="exp1">
<environment id = "exp1">
<transactionManager type = "JDBC" />
<dataSource type = "POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_shop" />
<property name="username" value="rapa_mybatis" />
<property name="password" value="pwd1" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/TestMapper.xml" />
</mappers>
</configuration>
dataSource에 커넥션을 어떻게 설정할 지에 대해 적는다. 여기서 제공하는 POOLED는 HikariCP가 아닌 mybatis에서 제공하는 자체 커넥션 풀이다.
MyBatis란?
MyBatis는 SQL 실행 결과를 객체로 자동 변환해주는 대표적인 SQL Mapper 프레임워크이다.
jdbc의 ResultSet 객체로부터 받아오는 값을 객체로 매핑하는 역할을 수행한다. XML에 SQL 쿼리를 직접 작성함에 따라 튜닝할 수 있고 JDBC 코드의 불필요한 반복을 제거할 수 있는 장점이 있다.
factory를 활용하여 session 구현
SqlSessionFactory는 MyBatis 전체 설정 정보와 실행환경을 내부에 포함하고 있는 설정 컨테이너 객체이다.
참고로 SqlSession은 쓰레드세이프를 지원하지 않는다. 이 경우 병렬작업을 시도하는 경우 오류 발생할 가능성이 높다. 그래서 한 쓰레드에 하나의 SqlSession을 사용하도록 하는 것이 좋다. spring의 경우에는 tomcat을 활용하면 요청 하나당 하나의 쓰레드를 생성해서 위 문제와는 무관하다고 볼 수 있다. 하지만 쓰레드를 이벤트마다 생성해주는 경우 예외처리를 잘 작성해야 할 것이다.
public class SessionFactory1 {
static void main(String[] args) throws Exception{
//sqlSessionFactory
//DB connection Settings
//실제 쿼리를 실행한다.
String xmlFileDir = "mybatis-config1.xml";
InputStream xmlInputStream = Resources.getResourceAsStream(xmlFileDir);
//MyBatis 전체 설정 정보와 실행환경을 내부에 포함하고 있는 설정 컨테이너 객체
//SqlSession 객체를 생성하는 객체
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xmlInputStream);
//실제 작업자
//SqlSession을 하면서 preparedStatement까지 포함한다
try(SqlSession sqlSession = sqlSessionFactory.openSession()){
DataSource dataSource = sqlSessionFactory.getConfiguration()
.getEnvironment()
.getDataSource();
System.out.println("dataSource" + dataSource.getClass().getName());
System.out.println("Sql 세션 획득 성공!");
//namespace.query_id
Integer result = sqlSession.selectOne("test.test");
System.out.println("result : "+ result);
}catch (Exception e){
throw new RuntimeException(e);
}
}
hikariCP로 설정을 변경하고 싶은 경우
<dataSource type = "config.HikariCpDataSourceFactory">
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_shop" />
<property name="username" value="rapa_mybatis" />
<property name="password" value="pwd1" />
</dataSource>
dataSource부분을 설정한 임의의 config파일에 설정한 hikariCP에 저장한 값을 가져올 수 있도록 설정
public class HikariCpDataSourceFactory implements DataSourceFactory {
private Properties properties;
@Override
public void setProperties(Properties props) {
this.properties = props;
}
@Override
public DataSource getDataSource() {
HikariConfig config = new HikariConfig();
config.setDriverClassName(properties.getProperty("driver"));
config.setJdbcUrl(properties.getProperty("url"));
config.setUsername(properties.getProperty("username"));
config.setPassword(properties.getProperty("password"));
return new HikariDataSource(config);
}
}
mapper을 활용한 SQL문 등록하기
mapper 파일에 원하는 sql문을 작성하여 이것을 가져와 사용하는 것이 가능하다.
resource의 mapper폴더 안의 s2폴더 안에 저장한 MemberMapper.xml파일에 튜닝한 sql문을 작성해놓는다. 단건 조회, 다건 조회와 같이 목적에 맞는 CRUD 작성한다. 그리고 sql문에 ?값으로 대응되는 것으로 #{이름}이 있는데 #{}로 선언하면 preparedStatement로 선언하는 것이라서 왠만하면 #{}을 사용하도록 하자.
<?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="mapper.s2.MemberMapper">
<select id = "findAll" resultType="domain.Member">
SELECT
id,
name,
password,
email,
balance
FROM
member
</select>
<select id = "findById" resultType="domain.Member" parameterType="long">
SELECT
id,
name,
password,
email,
balance
FROM
member
WHERE
id = #{id}
</select>
<select id = "findByEmail" resultType="domain.Member" parameterType="String">
SELECT
id,
name,
password,
email,
balance
FROM
member
WHERE
email = #{email}
</select>
<insert id = "save" useGeneratedKeys="true" keyProperty="id">
INSERT INTO member
(name, password, email, balance)
VALUES
(#{name},#{password},#{email},#{balance})
</insert>
<update id = "updateBalance">
UPDATE member
SET
balance = #{amount}
WHERE
id = #{id}
</update>
<delete id = "deleteById" parameterType="long">
DELETE FROM member
WHERE
id = #{id}
</delete>
<!-- resultType 체크-->
<select id = "count" resultType="Integer">
SELECT
COUNT(*)
FROM
MEMBER
</select>
</mapper>
mapper 패키지에 저장한 interface MemberMapper interface를 활용
mapper 패키지에 Member 테이블에 관해 MemberMapper을 만들어 interface를 활용한 프록시 객체를 만들 수가 있다.
public interface MemberMapper {
//CRUD
//다건조회
List<Member> findAll();
//단건조회
Optional<Member> findById(Long id);
Optional<Member> findByEmail(Long Email);
int save(Member member);
int updateBalance(@Param("id")Long id,@Param("amount") int amount);
int deleteById(Long id);
// 총 회원수
int count();
}
mybatis를 적용한 member 구현
앞의 설정한 mapper와 domain에 입력한 테이블의 정보를 가져와 구현
public class MemberCrud {
static void main() throws Exception {
MyBatisSessionConnector connector = new HikariMyBatisConnector(
"mapper.s2",
"domain"
);
try(SqlSession connection = connector.openSession(true)){
MemberMapper memberMapper = connection.getMapper(MemberMapper.class);
Member member1 = new Member(
null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber()
);
Member member2 = new Member(
null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber()
);
memberMapper.save(member1);
memberMapper.save(member2);
System.out.println("member1 id = "+ member1.getId());
System.out.println("member1 id = "+ member2.getId());
}
}
}
앞서 작성한 interface를 통해 작성한 프록시 객체를 활용함과 Dependency injection을 통해 유지보수가 쉽도록 구현했다.
try with resources문을 만들어 close문을 작성하지 않아도 sql 연결에 소모되는 자원을 안전하게 반납하도록 구현했다.
여기서 connector객체는 jdbc의 트랜잭션의 기능을 구현한다. 이때 만약 connector의 openSession(true)를 하지 않고 openSession()을 하면 기본값이 false로 되어있어 데이터베이스에 commit이 적용이 되지않아 insert, update, delete 작업은 반영이 되지 않는다.
예제에서 사용한 몇가지 디자인 패턴정리
디자인 패턴은 수많이 반복되는 설계 문제에 대해 검증된 해결방법으로 코드를 구조적으로 분리하고 확장성을 증가시켜 구현할 수 있고 협업 시 소통을 원활하게 할 수 있으므로 정리가 필요하다.
Proxy
실제 객체 대신 대리 객체를 사용하여, 접근을 제어하거나 공통 기능을 추가한 패턴이다. Spring의 AOP(관점지향 프로그래밍)이나 트랜잭션 처리에 핵심적으로 사용이 된다.
public interface MemberMapper {
//CRUD
//다건조회
List<Member> findAll();
//단건조회
Optional<Member> findById(Long id);
Optional<Member> findByEmail(Long Email);
int save(Member member);
int updateBalance(@Param("id")Long id,@Param("amount") int amount);
int deleteById(Long id);
// 총 회원수
int count();
}
인터페이스로 생성을 해주고
try(SqlSession connection = connector.openSession(true)){
MemberMapper memberMapper = connection.getMapper(MemberMapper.class);
Member member1 = new Member(
null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber()
);
Member member2 = new Member(
null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber()
);
memberMapper.save(member1);
memberMapper.save(member2);
}
위 코드처럼 인터페이스를 활용하여 객체에 부가기능을 더하여 구현
Factory
객체 생성 로직을 별도의 클래스 or 메서드에 위임하는 패턴이다. 이를 통해 클라이언트가 concrete class에 의존하지 않아도 되어 결합도를 낮추고 확장도를 높일 수가 있다.
Payment payment = new CardPayment();
이렇게 생성하면 코드가 concrete class에 의존을 하게 되어 새로운 타입을 추가하는 경우 코드의 변경이 필요하다.
오늘 공부한 코드에서는 다음과 같은 코드가 factory적 요소를 만족한다고 볼 수가 있다.
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(configuration);
객체 생성 로직을 SqlSessionFactoryBuilder에 위임하여 캡슐화를 하고 있으므로 Factory의 특징을 만족한다고 볼 수가 있다.
Builder
객체 생성 과정이 복잡할 때, 가독성과 안정성을 높이기 위해 단계적으로 객체를 생성하는 패턴
아까 Factory에서 사용한 코드도 builder 패턴을 가지고 볼 수가 있는데 실제로는 xml의 설정을 parsing하고 configuration -> Environment -> DataSource-> TransactionFactiory 설정 -> Mapper 등록 -> SqlSessionFactory 설정이라는 엄청 긴 과정을 한 줄로 캡슐화 과정을 거친 코드를 생성하여 복잡성을 낮추고 가독성을 높였다고 볼 수가 있다.
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(configuration);