'프로그래밍/데이터베이스(DB)' 카테고리의 글 목록 :: 잡다한 프로그래밍
반응형

mybatis Cursor란?

- 공식문서에 다음과 같이 설명하고 있다

A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.

(Cursor는 Iterator를 사용하여 Lazy하게 데이터를 가져오고 이는 List와 동일한 결과를 제공한다)

 

간단 정리 : 대량 데이터(대량 ROW)를 가져올때 사용하는 방법

- 약 1천만건의 데이터를 List에 담아서 어떠한 처리를 한다고 가정했을때 기존 방법은 OOM 발생 > CURSOR사용하면 해결 가능

 

1. cursor를 사용할때 동작방식의 차이

 

- 기존 방식의 동작 방법

1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비

2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행

3. 2.의 작업이 완료될때까지 코드 블로킹 (통상 service 코드 블로킹)

4. DB작업이 끝나면 spring 으로 돌아옴 (서비스로)

- Cursor를 이용했을 때 동작 방법

1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비

2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행

3. 트랜잭션을 시작, cursor의 경우 2의 작업이 cursor로 iteration을 반복할 수 있는 상태가 되면 모든 데이터가 받아지지 않더라도 DAO interface를 통해 cursor를 반환한다. (반환한 cursor를 서비스에서 처리 후 > 다시 반복)
4. DB 커넥션이 유지되는 동안 필요한 작업을 수행. cursor가 데이터셋의 끝에 도달할때까지 반복 가능

5. 트랜잭션 종료 (커넥션 종료)

 

 

3번에 따라 JVM 메모리에 한번에 모든 결과를 올려둘 필요가 없으므로, 충분한 시간만 주어진다면 조회 데이터 수가 많더라도 OOM 없이 데이터를 모두 읽어서 처리할 수 있다.

 

2. 사용 방법

기존 코드를 다음과 같이 개선하여 사용할 수 있다.

 

- 기존 Mapper 코드

List<TestDto> selectTest();

- 개선 Mapper 코드

Cursor<TestDto> selectTest();

 

- 기존 서비스 코드

@Service
@RequiredArgsConstructor
public class testService {
    @Autowired
    private TestMapper testMapper;

    public void test() {
    	List<TestDto> list = testMapper.selectTest();
        ...
        ....
    }
}

 

- 개선 서비스 코드

@Service
@RequiredArgsConstructor
public class testService {
    @Autowired
    private TestMapper testMapper;

	@Transactional
    public void test() {
    	try( Cursor<TestDto> list = testMapper.selectTest()) {
        	for (TestDto dto : list) {
            //....
            //..
            
            }
        } catch (Exception e) {
        	// ..
        }
    }
}

왜 이렇게 개선되어야 할까? 이는 앞서 말한 동작방식을 보면 알 수 있다.

Cursor 는 한줄씩 데이터를 처리할 수 있게 해준다고 앞에서 언급했는데, 이는 즉 데이터 처리가 끝나면 다음 줄을 읽어와야하는것을 의미한다.

따라서 전체 데이터를 모두 순회 할때까지 DB 연결이 유지되어야 한다는 걸 의미한다.

 

정리해보면

1. 해당 서비스 메소드에 @Transactional 을 달아 트랜잭션 상태를 유지시킨다

2. Service 메소드를 벗어나기전 Cursor를 써야하는 작업을 모두 마쳐야한다.

 

3. cursor와 fetchsize의 관계

네트워크 통신보다 메모리에 있는 내용을 처리하는 속도가 훨씬 빠름 따라서 얼만큼 처리할 데이터를 메모리에 올려놓는지 적절히 조율이 필요함

  • 통신 빈도를 줄인다 - 통신 한번에 받아올 데이터의 양이 늘어난다(캐시를 많이 해야하므로 JVM 메모리를 많이 먹는다)
  • 통신 빈도를 늘린다 - 통신 한번에 받아올 데이터의 양이 줄어든다(캐시를 적게 해도 되므로 JVM 메모리를 적게 먹는다)

 

4. 내가 겪었던 상황

 

테이블 A, 테이블 B의 데이터를 JOIN해서 select 해오고 있었음

A에 약 100만건, B에 1000만건의 데이터가 있다고 가정

 

JOIN하는 순간부터 너무 많은 시간이 걸림

반응형
반응형

1. 문법의 차이

#1) #을 이용한 경우

<select id="select" resultType="String" parameterType="Map">
    SELECT name FROM user WHERE id = #{id}
</select>

다음과 같은 SELECT문을 작성하였을경우 아래와 같이 ?에 파라미터가 바인딩 되어 수행된다 이렇게 파싱된 쿼리문은 재활용(캐싱)되므로 효율적이다.

SELECT name FROM user WHERE id = ?

 또한 변수에 작은 따옴표(')가 붙어 쿼리가 수행되므로 '#{id}'라고 쿼리문을 작성할 필요가 없다. 대신 다음과 같이 사용할 수 없다. 아래와 같이 사용할 경우 user_'tableName'이 되어버리므로 에러가 발생한다.

<select id="select" resultType="String" parameterType="Map">
    SELECT name FROM user_#{tableName} WHERE id = #{id}
</select>

 

#2) $를 이용한 경우

반면 $를 이용하게 되면 파라미터값이 바뀔 때마다 새로운 쿼리문의 파싱을 진행해야해서 성능상 단점이 존재한다.

또한 쿼리문에 #{}과 다르게 작은 따옴표(')가 붙지 않아서 테이블 이름이나 컬럼이름을 동적으로 결정할때 사용할 수 있다.

<select id="select" resultType="String" parameterType="Map">
    SELECT name FROM user_${tableName} WHERE id = #{id}
</select>

2. SQL Injection 차이

보안적으로 #과 $에는 차이가 존재한다. #은 $보다 보안에 안전하다 다음과 같은 예시를 보자

<select id="select" parameterType="Map" resultType="...">
    SELECT * FROM user WHERE id = '${id}' AND password = '${password}'
</select>

만약 이때 사용자가 id 값에 root' --를 입력했다고 가정하면 다음과 같은 결과를 초래한다.

SELECT * FROM user WHERE id = 'root' -- 'AND password = ''

사용자는 비밀번호를 입력하지 않았지만 뒷부분이 주석처리되어 로그인에 성공하게되어버린다 따라서 $는 #보다 SQL Injection에 취약하다

반응형
반응형

1. 사전 준비

- pom.xml 수정

다음과 같은 내용을 pom.xml에 추가합니다 1. mybatis (버전은 스프링 버전에 맞게 선택 가능) 2. mybatis-spring

3.dbcp2, 4. mysql-connector 5. spring-jdbc

		<!-- MySQL -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.5.3</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>2.0.3</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
			<version>2.7.0</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.47</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

 

- jdbc.properties생성

WEB-INF에 properties폴더 생성 > 다음과 같은 jdbc.properties 파일 생성

jdbc.username = user_id
jdbc.password = password
jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://URL/DB_NAME?serverTimezone=Asia/Seoul&useSSL=false

 

- mysql 연동

다음과 같이 root-context.xml에 추가한다 

	<!-- MySQL dataSource -->
	<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
	</bean>
    
	<!-- jdbc.properties를 찾게 해주는 placeholder -->    
 	<context:property-placeholder location="/WEB-INF/properties/jdbc.properties" />

2. mybatis 연동 및 사용

- TestModel 생성

DB로부터 받아온 데이터를 담을 Model 클래스를 생성한다

@Getter
@Setter
public class Test_Model {

	private String date;

	private int count;

}

 

- mybatis-config.xml 생성

main/resources에 다음과 같은 mybatis-config.xml을 생성한다. 등록된 모델을 test라는 별칭으로 사용할 수 있다.

<?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>
	<typeAliases>
		<typeAlias type="test.main.model.Test_Model" alias="test" />
	</typeAliases>

</configuration>

 

- rmcMapper.xml 생성

main/resource에 mappers라는 패키지를 만들고 패키지 내에 다음과 같은 testMapper.xml을 생성한다.

resultType를 config.xml에 지정한 test타입으로 반환하고 사용할 쿼리의 id를 getData로 지정한다.

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

<mapper namespace="test.main.mapper.testMapper">
	<select id="getData" resultType="test">
		select * FROM test_db
	</select>
</mapper>

 

- root-config.xml 수정

다음과 같이 root-config.xml에 mybatis를 설정하기 위한 위한 코드를 추가한다. mybatis-config.xml과 Mapper.xml을 bean으로 등록하고 sqlsession을 bean으로 등록한다.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:/mybatis-config.xml" />
		<property name="mapperLocations" value="classpath:mappers/**/*Mapper.xml"></property>
	</bean>
	
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate" destroy-method="clearCache">
		<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"></constructor-arg>
	</bean>
	

3. Controller, Service, DAO 만들기

- Controller 생성

다음과 같은 Controller를 작성한다. 간단하게 getData를 해오는 코드이다.

@Controller
@RequestMapping(value = "/brokenSVC")
public class testController {
	@Autowired
	private BrokenService testService;
	
	@GetMapping(value = "test")
	public String home(Model model) {
		System.out.println(testService.getdata().get(0).getDate());
		return "testpage";
	}
}

 

- Service 생성

다음과 같은 Service를 작성한다.

@Service
public class BrokenService {

	@Autowired
	private testDAO testdao;

	public List<Test_Model> getData() {
		return testdao.getData();
	}
}

 

- DAO 생성

다음과 같은 DAO를 작성한다.

@Repository
public class BrokenDAO {
	
	@Autowired private SqlSession sqlSession;
	private static final String Namespace = "test.main.mapper.rmcMapper"; //rmcMapper가 있는 위치
	
	public List<Test_Model> getData() {
		return sqlSession.selectList(Namespace+".getData");
	}
}

4. 결론

mybatis를 사용할 경우 기존 방법은 DAO에 쿼리문이 섞여있다는 단점이 있었는데 쿼리를 완벽하게 분리하여 사용할 수 있다는 장점이 있다.

반응형

+ Recent posts