분류

레이블이 JAVA인 게시물을 표시합니다. 모든 게시물 표시
레이블이 JAVA인 게시물을 표시합니다. 모든 게시물 표시

2023년 10월 4일 수요일

태블릿 YTMUSIC, 카카오톡, 분할화면 기능 안될 때 해결 방법

 개요 

'SAMSUNG TAB A8(2022)'에서 'Alldocube iPlay 50 Mini Pro Helio G99'로 넘어가면서 뭐 중요한 문제가 생겼습니다. 태블릿의 주 용도 중 하나인 음악 감상하며 가사를 볼 때 사용하는 분할 화면이 지원되지 않는 문제가 발생하였고, 카카오톡의 기능 중 채팅 목록과 채팅을 나눠서 보는 스플릿 분할 화면이 작동하지 않았습니다. 

기존과 같은 1080p 해상도인데 안 될 이유가 없다는 생각이 들었습니다. 안드로이드 개발도 2012년에 해봤기 때문에 기억을 되살려 개발자 도구를 받았습니다. 그리고 어렵지 않게 변경할 수 있었습니다. 

본래는 안드로이드 콘솔(ADB)와 드라이버를 받으면 쉽게 해결되지만 자칫 복잡해질 수 있어 덜 복잡한 방법으로 기술하겠습니다. 


준비물 

WINDOWS PC

DATA전송용 USB 케이블 

태블릿 

태블릿 과 pc를 usb로 연결 


1. 태블릿 환경구성 

태블릿 정보에서 빌드번호를 연타하여 개발자 옵션을 활성화 해 주세요 

개발자 옵션에서 usb 디버깅을 활성화 해주지 않으면 pc에서 ADB 를 통해 연결할 수 없습니다.  


2. PC 환경구성

안드로이드 STUDIO 다운로드 <--- 다운로드 링크 클릭 

설치는 뭐 인스톨이야 다음 다음 하시면 다 되는 거 아시죠? 

안드로이드 스튜디오를 실행한 후 신규 프로젝트를 생성합니다. 

프로젝트 생성방법 <-- 신규 프로젝트 추가 방법 클릭 

뭐 CREATE A NEW PROJECT와 대충 아무거나 누르면 되지만 까다로운 분이 계실까 봐 링크 첨부합니다. 

프로젝트가 생성되면 인터넷으로 파일들을 마구 받기 때문에 열릴 때까지 시간이 걸립니다. 다음 화면이 보일 때까지 일단 기다리세요.

개발자 도구 화면
저는 이 화면이 완전히 보일 때 까지 다운로드를 포함 약 30초 걸렸습니다. 걸리는 기간은 인터넷 환경에 따라 다를 수 있습니다. 

참고로 오른쪽에 장비창이 보이지 않는다면 상단 메뉴에서 
Tools 클릭 > Device Manager 클릭을 하시면 됩니다. 

주요 확인사항
안드로이드 스튜디오에서 기본 드라이버로 선택된 장치라면 위 화면에서 장치의 정식 명칭이 나오고 연결된 상태를 알리는 녹색 불이 켜집니다. 장치 명칭이 이상하고 녹색불이 켜지지 않는다면 드라이버가 맞지 않는 것 입니다. 
드라이버는 대게 os 개발 도구에 포함되어있으므로 sdk를 받으면 해결되는 편 입니다. 아래를 쭉 따라가세요 

SDK 및 Platform-Tool다운로드 

장치가 정상적으로 연결되지 않을 경우 'ADB'로 접속할 수 없습니다. 따라서 해당 장치 드라이버를 갖고 있는 'SDK'를 다운 받아야 합니다.

위 그림을 클릭해서 확대해보시고 순서대로 따라하시면 됩니다. 
Tools -> SDK Manager 클릭 

Search 입력난에 SDK 입력 -> Android SDK 를 차례로 선택하시고 태블릿 os에 맞는 버전을 선택 후 Apply를 누르시면 됩니다. 


그 다음으로 SDK Tools 에 들어가셔서 Android SDK Platform-Tools선택 후 Apply 한 후 Android SDK Location 복사해주세요 


windows + r 키를 누르시고 붙여넣기 하신 후 엔터 치면 입력된 경로로 이동합니다. 


위 그림처럼 platform-tools 폴더에 들어가시면 adb.exe 파일이 생성된 것을 확인하실 수 있습니다. 

이로서 준비는 끝났습니다. 

3. UI  크기 조절하기 

이제 powershell 창을 열어 adb.exe 파일이 있는 위치로 이동 합니다. adb 파일이 있는 위치를 복사해주시고 powershell 창에서 'cd 경로'  이런식으로 cd라는 커맨드에 한칸 떼고 붙여넣기 하시면 됩니다. 

순서 
windows + r -> powershell -> 엔터 
붙여넣기 -> 엔터 
dir -> 엔터 

해당 위치에 adb.exe 파일이 있는지 확인

이제 디바이스가 연결되었는지 확인하고 density 명령을 통해 화면 크기를 변경해주시면 됩니다. 
커맨드는
.\adb.exe shell wm density [수치] 형태로 이루어집니다. 


커맨드가 정상 작동하면 위 영상처럼 태블릿의 화면이 계속 변합니다. 
자신에게 맞는 사이즈를 찾았다면 글꼴이 너무 작거나 안 맞을 수 있으므로 해당 사이즈도 변경해주어야 한다. 

시스템 폰트를 본인에 맞게 조정한다.

powershell 창에서 
.\adb.exe shell settings put system font_scale [배율]f 를 입력

모든 조정이 끝났으면 .\adb.exe reboot 명령을 통해 재부팅을 수행한다. 

아참. Alldocube iPlay 50 Mini Pro Helio G99의 경우 이렇게 적용 할 경우 소프트웨어 제어버튼이 2개 나타나는데 런처를 설치하면 해결 됩니다. (저는 마이크로소프트 런처를 사용합니다.)

=====================================================
Density를 종종 해상도라고 하는 분들이 계신데 해상도는 size 라는 별도의 파라미터이고 
DPI라고 말씀하시는 분도 계신데 DPI 는 디스플레이의 물리적인 인치당 도트의 수입니다. 

Density 는 밀도라는 영문 단어입니다. 논리적으로 DPI와 매유 유사합니다. 하지만 엄연히 다른 수치입니다. 되도록 구분해서 써주시면 감사하겠습니다.
========================================================

이상입니다. 
누군가에겐 도움이 되었길 바라며 긴 글 마치도록 하겠습니다. 

2021년 5월 24일 월요일

spring boot기반 batch 프로젝트 생성부터 mybatis 연동 단순하게.

 개요 

회사에서 배치 프로세스를 분리해야 하는 일이 생겼습니다. 인터넷으로 이런 저런 조사를 해보는데 정상적인 문서를 찾기 힘든 것이 현실이고, 구했다 하더라도 입맛에 맞는 구조가 아닌 경우가 많아 최소한의 soruce를 통해 mybatis 연동(MariaDB)까지 정리 해보려 합니다. 

본문의 순서는 다음과 같이 진행 하겠습니다.

1. Eclipse spring Boot 설치 

2. Project 생성 

3. Project 환경 설정 

4. application.properties 설정 

5. Source 코드 생성 

6. 발생할 수 있는 오류

순으로 정리되어있습니다. 캡처를 할 경우 너무 여러 장을 찍어야 하기에 설치 순서는 동영상으로 대체하였습니다. 

그럼 시작하겠습니다. 


1. Eclipse spring Boot 설치 

일단 spring boot 프로젝트를 생성하기 위해서는 Eclipse의 Spring Tool이 필요합니다. Eclipse의 Help 버튼을 눌러 Market Place에 들어가서  Spring Tool을 검색해서 Install 하고 Eclipse를 다시 시작 합니다. 

Spring Tool 인스톨 영상 

2. Project 생성 

Project의 생성은 create a new project -> spring 검색 -> Spring Starter Project -> java 버전 선택 -> project 및 package 명 변경 -> spring batch 선택 -> Mybatis Framework 선택 -> finish 순으로 진행하였습니다. 

project 생성 영상 


3. Project 환경 설정


환경설정에 연관된 파일
환경설정에 연관된 파일은 2가지입니다. 
1) pom.xml을 통한 라이브러리 환경 설정과 
2) 프로젝트 환경 설정의 application.properties 파일입니다. 

1) 라이브러리 환경설정 

우선 저는 mariadb를 연결해야 하기에 mariadb의 maven 라이브러리를 pom.xml에 추가하겠습니다. maven을 통한 라이브러리 추가는 google 에서 maven +라이브러리 를 검색하거나 
메이븐 사이트 https://mvnrepository.com/

위 링크로 접속하셔서 해당 라이브러리를 검색하시면 쉽게 dependency 정보를 찾을 수 있습니다. 

mysql 드라이버 정보


검색된 메이븐 라이브러리중 자신에게 해당하는 버전을 클릭하면 이렇게 하단에 디팬던시 정보가 표기되어 해당 정보를 pom.xml에 붙여넣기 하면 됩니다.

pom.xml에 붙여넣기



4. application.properties 설정 

application.properties에 프로젝트에 사용할 환경 설정을 추가해줍니다. application.properties 는 spring toole에서 자동 완성을 지원합니다. 구동 화면은 아래와 같습니다. 
application.property 설정 영상

실제 application.properties에 수록된 내용은 다음과 같습니다. 
#데이터 베이스 연결에 사용할 드라이버 
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#데이터베이스 url 
spring.datasource.url=jdbc:mysql://192.168.0.1:6330/test?serverTimezone=UTC&characterEncoding=UTF-8
#계정 
spring.datasource.username=testUser
#비밀번호
spring.datasource.password=testUser

#mapper 파일이 적재될 위치 sql mapper 파일과 동일하게 맞춰준다.
mybatis.mapper-locations=com/springBatchTest/mapper/*.xml
#커스텀 환경변수 
custom.test=test-value


5. Source 코드 생성 

우선 application.properties에 설정한 위치에 mapper xml파일을 만들어줍니다. 
환경설정 순서(클릭시 커짐)
현재 프로젝트의 application 설정이 마이바티스 까지 연동되는 구조는 다음 순서에 따라 이루어집니다. 
application에서 모델을 연결해주는 것은 mapper location.
모델과 컨트롤러의 연결은 xml 파일내의 namespace가 classPath로 역할을 하게 되더군요. 

수정 및 생성한 소스프로그램은 총 4개의 파일입니다. 
1) testMapper.xml 
2) TestDataMapper.java
3) TestDataService.java
4) SpringBatchTestApplication.java
차례대로 기술하겠습니다. 

1) testMapper.xml 

mysql DB에서 데이터를 조회할 sql 문을 입력합니다. 또한 namespace에 java에서 연동할 인터페이스의 패키지 경로와 파일명을 입력합니다. 
<?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="com.springBatchTest.mapper.TestDataMapper">

<!-- 수행할 sql -->
    <select id="getTestDataList" resultType="java.util.Map">
<![CDATA[
SELECT 1 AS COL1 
UNION ALL 
SELECT 2 AS COL1
]]>
    </select>
</mapper>
sql은 간단하게 테스트를 수행할 수 있게 정의하였습니다. 

2) TestDataMapper.java

mapper.xml 에 정의된 위치에 정의된 명칭으로 생성해줍니다. 인터페이스 내부에는 xml에 정의된 sql과 동일한 명칭 그리고 동일한 데이터 형식을 갖고 있는 함수가 있어야 합니다. 
package com.springBatchTest.mapper;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface TestDataMapper {
List<Map<String, Object>> getTestDataList();
}
mybatis framework과 java Application간의 인터페이스 역할만 해주므로 정말 내용이 없습니다. SQL이 늘어나는 만큼 증가합니다. 

3) TestDataService.java

서비스에는 인터페이스에 정의된 형식과 동일한 서비스를 구성해줍니다. 이 파일 역시 자동 연결을 위한 선언 ,  서비스 선언 이외에 특별한 내용이 없습니다. 
package com.springBatchTest.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;

import com.springBatchTest.mapper.TestDataMapper;

@Service
public class TestDataService {
    @Autowired
    TestDataMapper testData ; 

    public List<Map<String, Object>> getTestDataList() {
        return testData.getTestDataList(); 
    }
}


4) SpringBatchTestApplication.java

package  com.springBatchTest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.List;
import  java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import  com.springBatchTest.service.TestDataService;

@SpringBootApplication
public class SpringBatchTestApplication {
public static void main(String[] args) {
try (ConfigurableApplicationContext ctx = SpringApplication.run(SpringBatchTestApplication.class , args)) {
SpringBatchTestApplication m = ctx.getBean(SpringBatchTestApplication.class );
m.selectDataf(); 
}catch (Exception e) {
// TODO: handle exception
System.out.println(e.getMessage());
}
}
  @Autowired
  TestDataService  testDataService;
    public void selectDataf() {
    List<Map<String, Object>> tmp = testDataService.getTestDataList();
    System.out.println(tmp.size());
    }
}


자 이제 테스트용 코드 생성은 끝났습니다. 

6. 발생할 수 있는 오류

1) 정상 처리 시 디버깅 콘솔 내용 


2021-05-24 19:28:03.528  INFO 10964 --- [           main] c.s.SpringBatchTestApplication           : Starting SpringBatchTestApplication using Java 15.0.1 on DESKTOP-3QN7REN with PID 10964 (D:\blogger\springBatchTest\target\classes started by jinyboys in D:\blogger\springBatchTest)
2021-05-24 19:28:03.533  INFO 10964 --- [           main] c.s.SpringBatchTestApplication           : No active profile set, falling back to default profiles: default
2021-05-24 19:28:06.259  INFO 10964 --- [           main] c.s.SpringBatchTestApplication           : Started SpringBatchTestApplication in 3.553 seconds (JVM running for 4.553)
2021-05-24 19:28:06.277  INFO 10964 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...

2021-05-24 19:28:06.771  INFO 10964 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2
2021-05-24 19:28:06.897  INFO 10964 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
945  INFO 10964 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

수행할 로직이 없기 때문에 약 3,4초만에 수행되고 종료되며 임시로 생성된 sql에서 받는 데이터 2건에 대한 사이즈 정보가 표기됩니다. 

2) application.properties내의 mapper 경로가 잘못 된 경우  

빨간색 글자처럼 파일을 찾을 수 없다는 메시지가 나옵니다. 
2021-05-24 19:31:39.543  INFO 7172 --- [           main] c.s.SpringBatchTestApplication           : Starting SpringBatchTestApplication using Java 15.0.1 on DESKTOP-3QN7REN with PID 7172 (D:\blogger\springBatchTest\target\classes started by jinyboys in D:\blogger\springBatchTest)
2021-05-24 19:31:39.553  INFO 7172 --- [           main] c.s.SpringBatchTestApplication           : No active profile set, falling back to default profiles: default
2021-05-24 19:31:42.462  INFO 7172 --- [           main] c.s.SpringBatchTestApplication           : Started SpringBatchTestApplication in 3.802 seconds (JVM running for 5.327)
2021-05-24 19:31:42.479  INFO 7172 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...

2021-05-24 19:31:42.987  INFO 7172 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-05-24 19:31:43.074  INFO 7172 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2021-05-24 19:31:43.086  INFO 7172 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
Invalid bound statement (not found): com.springBatchTest.mapper.TestDataMapper.getTestDataList

3) mapper.xml 파일 내부 SQL구문의 ID와 mapper.java 인터페이스가 불일치 할 경우

위와 동일한 오류가 발생합니다. 
 Invalid bound statement (not found): com.springBatchTest.mapper.TestDataMapper.getTestDataList

4) service.java 클래스 내에 @Autowired 설정이 없는 경우

Cannot invoke "com.springBatchTest.mapper.TestDataMapper.getTestDataList()" because "this.testData" is null

5) mapper.java 클래스가 없거나 비정상 , @mapper 설정이  없는 경우

Description:

Field testData in com.springBatchTest.service.TestDataService required a bean of type 'com.springBatchTest.mapper.TestDataMapper' that could not be found.

The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)


Action:

Consider defining a bean of type 'com.springBatchTest.mapper.TestDataMapper' in your configuration.

Error creating bean with name 'springBatchTestApplication': Unsatisfied dependency expressed through field 'testService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testDataService': Unsatisfied dependency expressed through field 'testData'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.springBatchTest.mapper.TestDataMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}


이상 스프링 배치 프로젝트 생성부터 mybatis 연동까지 그리고  발생할 수 있는 오류까지 알아봤습니다. 

이렇게 쉽게 4개의 파일만 설정하면 연동할 수 있는 기본적인 형태를 이해하지 못하고 엉뚱하게 정리 해 놓은 글들이 너무 많아 직접 정리를 해 보았습니다. 누군가에게 도움이 되면 좋겠네요 ㅎㅎ. 

2021년 4월 2일 금요일

java runtime exec() 에서 curl post 사용 오류 및 해결

 개요 

오픈 소스 기반 프로그램 수행을 위해 curl 커맨드를 외부 데이터베이스와 연결하여 결과를 받는데 windows 환경에서는 잘 되었지만 linux 기반에 동일한 tomcat과 java 환경에서 수행 시 오류가 발생하였습니다. 오류 메시지는 종류도 다양하지만 주로 아래와 같은 메시지가 나왔습니다. 

보내는 메시지는 다음과 같았습니다. 

String queryTable = "curl -X POST -H \"Content-Type:application/json\" " + " -d  @" + filePath + " " + serverUrl;

형태의 비교적 단순한 문장이었고, 쿼리를 담은 파일을 포함하여 발송합니다. 

여기서 filePath 는 데이터를 전송하기 위해 생성한 데이터 파일의 절대경로입니다.  serverUrl은 전송 대상 서버의 전체 url 입니다. 

옵션 중 -d 뒤의 @ 옵션을 제거하면 파일이 아닌 문자 데이터 형태로 전송할 수 있습니다. 

에러 메시지는 대략 모두 비슷한 유형으로 <h1>이 H4로 바뀌는 정도의 오류였습니다. 

 illegal character varchar='" 

<h1>bad message 400

windows 기반의 tomcat9에서 수도 없이 많은 테스트를 거친 프로그램이기에 믿고 리눅스에 맡겼더니 배신도 이런 배신이 없습니다. 


문제코드 

여러 검색 결과 문제는 java 자체의 Process 클래스에 의한 처리였습니다. 외부 명령을 실행하기 위해 다음과 같은 코드를 작성하여 사용해왔습니다. 

public static String executeCommand(String command) {
StringBuffer output = new StringBuffer();
Process p;
String outString = ""; 
try {
p = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new  InputStreamReader(p.getInputStream()));
String line = "";
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
p.waitFor();
outString= output.toString();
            if (p.exitValue() != 0) {
                // shell 실행이 비정상 종료되었을 경우
            BufferedReader errorBufferReader = null; 
            StringBuffer errorOutput = new StringBuffer();
                errorBufferReader = new BufferedReader(new InputStreamReader(p.getErrorStream(), "EUC-KR"));
                String msg  = "";
                while ((msg = errorBufferReader.readLine()) != null) {
                    errorOutput .append(msg + System.getProperty("line.separator"));
                }
            }
 
} catch (Exception e) {
logger.error(e.getMessage());
}
return output.toString();
}

상위 스크립트 처럼 java 자체의 process runtime exec를 사용 할 경우 linux나 macos 환경에서 알 수 없는 오류로 고생하게 됩니다. 또한 java 자체의 processbuilder를 활용하더라도 동일한 현상이 나타나는 것을 확인하였습니다. 

여러 문서들을 검토 한 결과 Apache commons common-exec 를 사용할 경우 해당 문제가 해결된다고 합니다. 


https://commons.apache.org/proper/commons-exec/

위 라이브러리를 직접 다운 받아 사용하시면 됩니다. maven 환경이라면 dependency에
아래와 같은 항목을 추가해주세요
pom.xml
<dependency>  
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-exec</artifactId>
    <version>1.3</version>
</dependency>  

해결 코드 

package Controller;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class apCmdLine {

private static final Logger logger = LoggerFactory.getLogger(apCmdLine.class);
 
/**
  * linux 에서 shell script가 비정상 실행되는 오류에 대한 fix 
  * @param command
  * @return
  * @throws Exception
  */

 public String execAndRtnResult(CommandLine command) throws Exception {
      String rtnStr = "";
              DefaultExecutor executor = new DefaultExecutor();
      ByteArrayOutputStream bs = new ByteArrayOutputStream();
      PumpStreamHandler streamHandler = new PumpStreamHandler(bs);
      executor.setStreamHandler(streamHandler);
      int exitCode = executor.execute(new CommandLine(command));
      rtnStr = bs .toString();
      logger.info("exitCode : " + exitCode);
      logger.debug("outputStr : " + rtnStr );
      return rtnStr;
}
}


추가오류 

1. 전송 메시지 출력 

linux 환경에서 정상적으로 데이터 처리가 되고 난 이후 rtnStr로 받은 결과에 위 사진과 같은 메시지가 중간중간에 끼어드는 문제가 발생하였습니다. 
r -   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100    90  100    70  100    20  14629   4179 --:--:-- --:--:-- --:--:-- 17500

이 문제를 해결 하기 위해 JSON 스트링이 아닌 데이터에 대한 필터를 고려해보았으나, 라인단위의 중간에 해당 데이터가 끼어드는 경우도 발생하였습니다. 검토 결과 CURL로 데이터를 전송할 경우 전송 결과에 대한 알림이 발생한다고 합니다. 이것 역시 WINDOWS 환경에선 발생하지 않습니다. 

검색에 시간은 제법 들었고 필터도 해보고 여러 삽질을 해봤지만 실제 고치는 방법은 매우 단순했습니다. URL 앞에 -s만 붙여주면 되는 일이었습니다.  

보내는 메시지에 대상 서버 주소 앞에 -s 옵션을 붙여줍니다. -s 는 silent 시스템 메시지를 보내지 마라는 명령어입니다. 
String queryTable = "curl -X POST -H \"Content-Type:application/json\" " + " -d  @" + filePath + " -s " + queryServer; 

비슷한 유형을 정리해 놓은 글을 링크합니다. (영문입니다.)

2. 메시지 길이가 길어 스트림을 초과 

이런 경우는 흔치 않을 것 같습니다. json 형태의 데이터를 보내고 그 결과를 받다보니 curl에서 전송할 수 있는 범위를 넘어가더군요. 길이가 길 경우 교환 데이터를 파일 형식으로 보내면 해당 문제는 사라집니다. 하지만 이 방법 역시 문제가 있습니다. json 파일 형태로 보내다보니 파일을 작성한 시점에 동시 트랜잭션이 여러 건 일어나고 서로 다른 데이터를 요구했을 경우 해당 트랜잭션에서 내가 원하는 데이터가 정말 갔다는 것을 보장할 수 없죠. 결국은 runtime exec를 통해 데이터를 교환하는 것 보단 httpconnection을 통해 데이터를 교환하는 것이 적절한 처리 방법으로 생각됩니다. 


이상입니다. 
읽어주셔서 감사합니다. 
읽으신 분에게 도움이 되면 좋겠네요. 

2019년 10월 29일 화요일

egovframework Datasource 여러개 사용

1. 개요 

몇개의 프로젝트를 통합하는 작업을 진행하게 되었습니다. SK프레임워크와 전자정부 프레임워크 2.0으로 되어있는 녀석인데 서로 다른 프로젝트를 통합하다보니 다양한 데이터베이스에 대한 지원이 필요했습니다. 그리고 앞으로도 자주 있을 작업 같은데 까먹을 것 같아 적어봅니다.

2. 설정방법 

설정방법은 간략하게 파일별로 기술하겠습니다.

가) context-datasource.xml 

우선 연결에 사용할 데이터의 환경정보를 입력합니다. 

<? xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http:www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<bean id="데이터소스ID1" class="org.apache.commons.dbcp.BasicDatasource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver">
    <property name="url" value="데이터베이스 접속정보1">
    <property name="username" value="접속id">
    <property name="password" value="접속비번">
</bean>

<bean id="데이터소스ID2" class="org.apache.commons.dbcp.BasicDatasource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver">
    <property name="url" value="데이터베이스 접속정보2">
    <property name="username" value="접속id">
    <property name="password" value="접속비번">
</bean>

</beans>

나) context-sqlMap.xml

다음은 접속정보와 sqlMap을 연동해줍니다.
이때 주의해야 할 점은 데이터 소스별로 SqlMapClient의 경로를 다르게 지정해야 합니다.
<? xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xmlns:tx="http://www.springframework.org/schema/tx"
      xmlns:util="http://www.springframework.org/schema/util"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http:www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">


<bean id="클라이언트ID1" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocations">
        <list>
            <value>classpath:/egovframework/경로1/*.xml</value>
        </list>
    </property>
    <property name="dataSource" ref="데이터소스ID1" />
</bean>

<bean id="클라이언트ID2" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocations">
        <list>
            <value>classpath:/egovframework/경로2/*.xml</value>
        </list>
    </property>
    <property name="dataSource" ref="데이터소스ID2" />
</bean>
</beans>

다) DAO설정

egovFramework에서는 기본 datasource로 설정된 데이터베이스를 이용하는 것 같습니다. 따라서 첫번째 datasource로 지정된 데이터베이스를 사용 할 경우 DAO에 작업을 하지 않아도 자동으로 사용됩니다. 반면 2번째 데이터 소스를 사용 할 경우 DAO클래스에 선언을 해주어야 합니다. 물론 DAO가 달라지므로 IMPL과 SERVICE를 모두 작성해주어야 합니다.
package egovframework.rte.....
import ......

@repository("DAO이름")
public Class DAO이름 extends EgovAbstractDAO{

  @Resource(name = "클라이언트ID2")
  public void setSuperSqlMapClient(SqlMapClient sqlMapClient){
    super.setSqlMapClient(sqlMapClient);
  }

  public List .......
      ...
      ...

3.잘못된 설정으로 발생할 수 있는 오류

가) DAO에 sqlMapCLient 를 등록하지 않고 SQL을 호출한경우

등록된 SQL맵의 ID를 찾을 수 없습니다. 라는 메시지가 화면 호출시 발생합니다. 
 There is no statement name 호출된 SQL ID in this SqlMap
DAO 클래스의 내용을 확인해보세요.

나) SqlMapConfig에 잘못된 파일명이나 경로를 등록한경우 

sqlMapConfig/sqlMap 경로에 작성된 파일을 점검해보세요 .
 Could not find resource egovframework/sqlmap-xml/......
context-sqlMap.xml 파일에 등록되어있는 xml파일중 비정상적인 경로를 가진 xml이 있습니다.

다) context-sqlMap.xml 파일에 잘못된 정보가 기입된경우

SQL맵을 찾을 수 없다는 오류가 발생할 수 있습니다.
Error creating bean with name "클라이언트ID2" defined in file ......

라) dataSource의 정보가 잘못된경우 

커넥션 오류가 발생합니다. 접속정보를 확인 하시면 됩니다.
The Connection descriptor used by the client was : ......

마)sql문에 바인드 된 클래스가 잘못된경우

각 sql에서 오류가 발생합니다.  이 경우 오류가 발생한 파라미터로 class가 바인드된 sql map에서 클래스의 경로를 확인해주세요.
java.lang.runtimeEception : Error parsing XPath 'sqlMapConfig/sqlMap': ......
java.lang.RuntimeException : Error parsing XML. Cause: java.lang.RuntimeException : Error Parsing Xpath '/sqlMap/select'. Cause: com.ibatis.sqlmap.client.SqlMapException : 
java.lang.RuntimeException : Error parsing XML. Cause: java.lang.RuntimeException : Error Parsing Xpath '/sqlMap/delete'. Cause: com.ibatis.sqlmap.client.SqlMapException : 
java.lang.RuntimeException : Error parsing XML. Cause: java.lang.RuntimeException : Error Parsing Xpath '/sqlMap/update'. Cause: com.ibatis.sqlmap.client.SqlMapException : 
java.lang.RuntimeException : Error parsing XML. Cause: java.lang.RuntimeException : Error Parsing Xpath '/sqlMap/insert'. Cause: com.ibatis.sqlmap.client.SqlMapException : 


이상입니다.
읽어주셔서 감사합니다.

2019년 2월 21일 목요일

대용량 데이터 처리 1. java 다중쓰레드 활용

1. multi thread 는 어디에 씁니까? 

개인적인 용도는 이렇습니다.

데이터 처리 위주의 웹서비스를 운영하다 보면 항상 발생하는 문제가 있습니다. 데이터 처리 시간이 지연되면서 발생하는 web의 응답 지연 현상. 단순하게 select 구문을 사용한 데이터 조회라면, SQL을 수정하면 되지만, 그렇지 않을 경우가 있습니다.

바로 대용량 text, json, excel 파일을 입력하는 경우 발생하는 문제입니다. 이중 가장 처리시간을 획기적으로 줄일 수 있는 것은 EXCEL입니다. EXCEL 파일은 스트림을 열어 최대 로우수와 시트수를 확인하고 여러 쓰레드로 분할하여 처리 할 수 있기 때문입니다.

그리고 TEXT, CSV, JSON파일의 경우 파일의 사이즈를 알 수는 있지만, 최대 행수를 미리 알 수 있는 방법이 없어 단일 스레드 처리를 하게 되며 처리 시간이 더욱 증대 되는 문제가 있습니다. 하지만 WEB에서 많이 사용하는 방식이기도 합니다.

WEB서비스는 SESSION의 제한 시간을 30분으로 설정하고 있는 경우가 많습니다. 세션 시간이 30분으로 제한되어있는 서비스의 경우 웹을 통한 요청이 30분이 초과되면 해당 프로세스를 종료시키는 로직이 추가되는 경우가 많기에 대용량의 CSV, TEXT 파일의 처리에는 대게 WEB서비스에서 분리시키는 쓰레드를 사용하게 됩니다.


2.multi thread 사용 시 주의사항 

WAS 서버의 환경을 우선 알아야 합니다. 대게 2가지 정도만 알면 되는데 적용할 서버의 HEAP SPACE와 THREAD POOL의 개수입니다. 둘 중 하나가 초과될 경우 심각한 오류와 함께 서버가 셧다운 되는 현상이 발생할 수 있으므로, 서버관리자에게 문의 후 결정하는 것이 좋습니다.

프로그래밍 시에도 thread를 무한정 늘리는 코드를 만드는 것은 하지 않는 것이 좋습니다.

일반적으로 가장 많이 범하는 오류가 thread.sleep(1000); 명령을 통해 쓰레드의 종료시간을 예상하여 쓰레드의 개수를 유지하는 방법입니다. 이런 방법으로 프로그래밍 할 경우 다양한 외부 환경의 변화로 인해 처리 속도가 지연될 경우 역시 시스템의 셧다운이 발생할 수 있습니다.

3. thread에 대한 설명

그래서 간략하게 몇 가지만 이야기 하겠습니다. 

1) 개념 적 이야기 

대략적인 내용을 그림으로 그리자면 아래와 같습니다. 


메인 쓰레드에서는 동시 처리를 수행할 작업량과, 수행 작업을 담은 작은 단위의 작업을 생성합니다. 생성된 작업은 메모리(힙 스페이스)에 적재되고, 작업 대기 중인 쓰레드에 할당되어 작업 처리를 수행합니다. 위와 같은 개념적인 구조를 갖고 있기에 다중 쓰레드 처리를 잘못 할 경우 쓰레드 문제가 발생하는 것 입니다. 

2) 흔히 발생하는 오류 

발생하는 문제는 대게 아래와 같습니다.

1. java.lang.OutOfMemoryError : unable to create new native thread 
  at java.lang.Thread.start0(Native Method)
  at java.lang.Thread.start(Thread.java:597)
  at java.util.concurrent.ThreadPoolExecutor.addifUnderMaximumPoolSize(ThreadPoolExecutor.java:727)
  at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:657)

2. java.lang.OutOfMemoryError : Failed to create a thread retVal -1073741830, ernrno 11
  at java.lang.Thread.startImpl(Native Method) 
  at java.lang.Thread.start(Thread.java:886)

※ 1,2는 유사한 오류입니다. 너무 많은 쓰레드가 한 번에 호출되어 더 이상 쓰레드를 만들 수 없음. (시스템 사양에 따라 다른 것 같습니다. 개인 pc는 600개까지 되며 was 시스템에서 200개 까지 확인하였습니다.) 즉. 쓰레드의 종료 처리가 되지 않아 발생하는 문제 입니다. 

3. "Thread-20" java.lang.OutOfMemoryError : java heap space 
※ 개별 쓰레드에 할당된 작업의 크기가 heap 스페이스의 크기를 초과했을 경우 발생합니다. 

위 오류들의 패턴으로 봤을 때 쓰레드의 오류는 대게 할당된 메모리 영역을 벗어났을 때 발생하는 것 같습니다. 1,2번의 경우 할당할 수 있는 쓰레드 개수를 초과할 경우 (제 개인 pc에서는 600개의 쓰레드가 할당되지만, 타 시스템 적용시 200개밖에 되지 않음), 그리고 3번은 쓰레드에 할당할 힙 메모리가 없을 경우 발생했습니다.

4. thread 코드 작성

JDK 8버전입니다.
멀티 쓰레드를 호출할 메인 클래스 생성 TestMain.java 
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class TestMain {

static int maxThreadCnt = 4; //최대 쓰레드 수 
static int inPutRowCnt = 1000; //쓰레드별 입력행수
static boolean threadState = true; //쓰레드 그룹개수제한
        //쓰레드 그룹을 컨트롤 할 서비스 생성
private static ExecutorService ex = Executors.newCachedThreadPool(new ThreadFactory(){
@Override
public Thread newThread(Runnable r){
return new Thread(r);
}
}); 

//파일 처리를 위한 메인 쓰레드 
public static void main(String[] arg) {
String fPath = "D:/testwork/testdata/IFS_02-11-2019 20-54-22-00_timeSeries.dat"; 
try {
FileReader rd = new FileReader(fPath);
BufferedReader br = new BufferedReader(rd); 
String readLine; 
int rowcount = 0; 
ArrayList<String> list = new ArrayList<String>();
while ((readLine= br.readLine()) != null) {
rowcount++;
list.add(rowcount+","+readLine); 
if(rowcount%inPutRowCnt == 0){ 
//설정된 파일 행 수 만큼 분할하여 개별 쓰레드에 적재
Runnable r = new Testsub((ArrayList<String>) list.clone()); 
threadManage(r); 
list.clear();
}
}
//미처리 된 데이터에 대해 처리 
if(list.isEmpty()){
System.out.println("파일 처리 완료 ");
}else{
Runnable r = new Testsub((ArrayList<String>) list.clone());
r.run();

}

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

//쓰레드 관리를 위한 쓰레드 관리함수 
private static void threadManage(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
               
//활성중인 쓰레드의 개수가 최대 쓰레드 개수보다 작을 경우 EXECUTOR사용
if(t.activeCount() < maxThreadCnt && threadState){
System.out.println("Active Thread count is : "+t.activeCount());
ex.execute(t);
}else{
//최대 쓰레드 개수에 도달 시 executor에게 작업완료 후 종료명령
if(ex.isTerminated()){
//활성 쓰레드가 모두 종료되었을 경우 새 Executor 할당 후 실행
threadState = true;
ex = Executors.newCachedThreadPool(new ThreadFactory(){
@Override
public Thread newThread(Runnable r){
return new Thread(r);
}
}); 
ex.execute(t);
}else{
if(threadState){  //쓰레드 종료 명령이 호출되지 않은 경우만 수행
threadState = false;
System.out.println("Thread group Shutdown");
ex.shutdown();
}
t.run(); //쓰레드 종료 명령이 완료될 때까진 메인 클래스가 직접수행 
}
}
}
}

하위 생성될 보조 쓰레드 Testsub.java
import java.util.ArrayList;
import java.util.HashMap;

public class Testsub implements Runnable{

ArrayList<String> pList; 

public Testsub(ArrayList<String> arrayList) {
pList = arrayList; //전달받은 파라미터를 할당
}

@Override
public void run() {
ArrayList dataArray = new ArrayList();
for(int i = 0; i < pList.size(); i++){
  //개별 비즈니스 로직을 탑재 
String[] strArr = pList.get(i).split(","); 
HashMap<String, Object> tmpMap = new HashMap<>();
tmpMap.put("0", strArr[0]);
tmpMap.put("1", strArr[1]);
tmpMap.put("2", strArr[2]);
tmpMap.put("3", strArr[3]);
tmpMap.put("4", strArr[4]);
dataArray.add(tmpMap);
}
//DB적재를 위한 코드 작성해서 사용 
System.out.println("work complete dataCnt : " + dataArray.size());
}
}

수행결과 콘솔
Active Thread count is : 1
Active Thread count is : 2
Active Thread count is : 3
Thread group Shutdown
work complete dataCnt : 1000
work complete dataCnt : 1000
work complete dataCnt : 1000
work complete dataCnt : 1000
....
....
work complete dataCnt : 150
파일 처리 완료 
work complete dataCnt : 1000

※ 참고사항
TestMain.java
일단 스트림에서 바로 데이터를 처리하지 않는 이유는 단건 insert의 경우 부하가 심하고, 데이터에 대한 비즈니스 로직이 탑재 될 경우 해당 연산처리 덕에 메인 프로세스가 느려지는 것을 방지하기 위해 메인 클래스에서는 데이터를 분할하여 전송만, 보조 쓰레드에서는 데이터를 입력하는 비즈로직을 수행하는 방식을 취하고 있습니다.

ExecutorService ex 를 사용하는 이유는 수행되고 있는 쓰레드를 그룹화 하여 수행 shutdown() 명령 하나로 '작업 완료 후 종료해' 라는 명령을 간결하게 내릴 수 있습니다. JDK나 WAS시스템의 환경에 따라 쓰레드의 개수를 제어하기 편리 합니다.

shutdown()의 명령 이후에는 isTerminated() 명령을 사용하여 쓰레드의 종료를 확인해야 합니다. 서비스의 종료가 완료되지 않은 상태에서 새 서비스를 추가 할 경우 TestSub.java에서 수행되는 개별 비즈니스 로직의 수행 속도에 따라 쓰레드가 무한하게 증식할 수 있습니다. 

Runnable r = new Testsub((ArrayList<String>) list.clone()); 
러너블은 생성하는 하는 단계에서 파라미터를 전달할 수 있습니다. 전달할 수 있는 것은 파라미터뿐만 아니라 SPRINGFRAMEWORK에서의 SERVICE도 가능합니다. VO도 가능하겠죠? (이 부분은 SQL 사용을 위한 Service만 전달해봐서 정확히 모르겠습니다. ) 

또한 list 자체를 전달하는 것이 아닌 list.clone()으로 복제를 생성하는 이유는 가공중인 상태의 리스트를 던질 경우 바로 뒤에 오는 초기화 부분 list.clear(); 에서 초기화 되면서 리스트에 담겼던 데이터가 사라집니다. 

쓰레드 호출 시 t.setDaemon(true);를 사용한 이유는 이상한 서버의 오류 때분입니다. 자바의 쓰레드는 데몬 쓰레드 이건 아니건 종료되면 가비지 컬렉터에 의해 자원이 회수되는 것이 정상이지만, 일부 was 서버에서 사용자 쓰래드가 종료되지 않아서 할당된 쓰레드가 모두 종료되지 않는 오류가 있었습니다. 그 이후 이렇게 쓰고 있습니다.

Testsub.java
Thread를 만든다더니 Runnable을 만든 이유는 상속이나 수행 측면에서 이용이 편리해서입니다. 

dataArray 를 사용하는 이유는 대량의 데이터 입력 시oracle.jdbc.driver.OracleDriver 의 배열 처리가 한 건씩 데이터 입력 SQL을 수행하는 것 보다 수백 배 속도가 빠르기 때문 입니다. 

1건씩 insert 처리 시 접속 -> SQL전송 -> 커밋 -> 접속해제를 반복하기에 자원의 소모가 심합니다.  그리고 속도도 느리기 때문에 스프링 환경에선 array, 기타 환경에선 executeBatch를 사용하는 것을 개인적으로 선호합니다. 

프로그램 구동 영상 (뭐 볼 필요 없겠지만 첨부합니다.)

이상입니다. 수정이나 의견이 있으신 분은 댓글 달아주시면 고맙겠습니다.

연관된 문서가 있습니다.
대용량 데이터 처리 1. java 다중쓰레드 활용
대용량 데이터 처리 2. 오라클 병렬처리 parallel
대용량 데이터 처리 3. 테이블 파티셔닝
대용량 데이터 처리 4. DBMS_JOB