플젝을 하면서 PDF 문서만들기를 하게 되었다. 주어진 이미지들을 하나의 PDF 문서로 만드는 것인데 구글링 하니 잔뜩 나오고해서 내 입맛대로 소스 변경 후 기록용으로 작성한다.

 

우선 사용하는 라이브러리는 PDFBox라는 놈이다. Maven Dependency는 다음과 같다.

1
2
3
4
5
6
<!-- Apache PDF Box -->
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.18</version>
</dependency>
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static String makeImgPdf(File[] imgDir , String pdfDir) throws Exception {
    String result = "";
    //PDF 문서를 생성함
    PDDocument doc = new PDDocument();
    try {
        //Post를 통해 생성할 이미지를 가져옴
        File[] imgFiles = imgDir;
        
        //첨부된 이미지 파일 갯수만큼 반복문 실행
        for ( int i = 0; i<imgFiles.length; i++) {
        //이미지 사이즈 확인
        Image img = ImageIO.read(imgFiles[i]);
        
      //PDF 페이지를 생성함
       PDPage page = new PDPage(PDRectangle.A4);   
       // 그래서 PDF 문서내에 삽입함
        doc.addPage(page);
         
        //PDF에 삽입할 이미지를 가져온다. 인자값 - 이미지경로, PDF 페이지
        PDImageXObject pdImage = PDImageXObject.createFromFile(imgFiles[i].toString(), doc);
        //현재 설정된 PDF 페이지의 가로/세로 구하기
        int pageWidth = Math.round(page.getCropBox().getWidth());
        int pageHeight = Math.round(page.getCropBox().getHeight());
        
        //이미지 가로사이즈가 PDF 가로사이즈보다 클 경우를 대비해서 이미지 리사이징 실행
        //현재 설정된 PDF 페이지 가로 , 이미지 가로 사이즈로 비율 측정 
        float imgRatio = 1;
        if ( pageWidth < img.getWidth(null)) {
            imgRatio = (float)img.getWidth(null/ (float)pageWidth;
            System.out.println(">>> 이미지 비율 : " + ratio);
        }
        
        //설정된 비율로 이미지 리사이징
        int imgWidth = Math.round(img.getWidth(null/ imgRatio);
        int imgHeight = Math.round(img.getHeight(null/ imgRatio);
        
        //이미지를 가운데 정렬하기 위해 좌표 설정
        int pageWidthPosition = (pageWidth - imgWidth) / 2;
        int pageHeightPosition = (pageWidth - imgWidth) / 2;
        
        PDPageContentStream contents = new PDPageContentStream(doc, page);
        //그 콘텐츠에 이미지를 그린다. 인자값 - X/Y/가로사이즈/세로사이즈
        contents.drawImage(pdImage, pageWidthPosition, pageHeightPosition, imgWidth, imgHeight);
        //콘텐츠에 이미지를 다 그렸으면 콘텐츠를 종료
        contents.close();
        //그린것이 끝났으니 해당 문서를 저장.
        doc.save(pdfDir);
        
 
        }
    } catch (Exception e) {
        System.out.println("Exception! : " + e.getMessage());
    }
 
    try {
        doc.close();
        result = "success";
    } catch (IOException e) {
        result = "error";
        e.printStackTrace();
    }
 
    return result;
}
cs

최대한 주석을 많이 달아놨다.

 

PDF를 만들면서 가장 신경쓴 부분이 이미지의 리사이징 및 가운데정렬 부분이었다.

27번 라인부터 시작하는데, PDF의 가로 길이와 이미지의 가로길이를 나눠준 비율만큼 이미지 세로 비율도 축소시켜버리는 것.

그리고 그렇게 축소된 이미지를 PDF의 가운데정렬을 해야했기 때문에 38번 라인에서 PDF의 가로만큼 이미지 가로 차이를 계산해준다. 그리고 반으로 나눠주면 PDF 내 이미지가 양쪽 여백이 계산될 것이다. 마찬가지로 PDF의 세로만큼 이미지 세로 차이를 계산한 후, 반으로 나눠주면 PDF 내 이미지의 상하 여백이 계산되는 방식.

 

일단은 전체적으로 잘 나온다.

추후에 고려할 부분은 옵션에 따라 PDF 용지의 가로방향/세로방향을 설정할 수 있게 하는 부분인데, 이 또한 크게 어렵지 않으니 금방할 수 있을 것으로 보인다.

 

+결과

PDF의 배경이미지 사이즈는 가로 595, 세로 842이다

이미지는 가로 3000, 세로 3600으로, 계산시 이미지 비율이 약 5.042017이 나온다.

즉, 이미지 축소가 없었으면 PDF 전체를 덮고도 남았을것이나, PDF 가로 비율에 맞춰 축소가 성공적으로 진행되었으며,

위의 로직과 같이 상하 여백이 동일하게 되어 가운데 정렬이 성공적으로 되었음을 알 수 있다.

 

1
2
3
4
5
>>> pageWidth : 595
>>> pageHeight : 842
>>> 이미지 비율 : 5.042017
>>> imgWidth : 3000
>>> imgHeight : 3900
cs

'어장 Develop > 어장 JAVA' 카테고리의 다른 글

[poi] java poi 를 활용한 엑셀/워드 텍스트 추출  (0) 2022.07.04
[Tomcat 9.x] JNDI설정  (0) 2022.02.21
[xml] RestAPI XML Return (2)  (0) 2021.07.06
[xml] RestAPI XML Return  (0) 2021.07.02
[SpringBoot] Async 사용하기  (0) 2021.06.25
블로그 이미지

김생선

세상의 모든것을 어장관리

,

지난번에 XML RestAPI Return에 대해 포스팅을 하자마자, 실제 URL 호출 테스트를 해보니 웬걸 내가 전달받은 형태와는 판이하게 달랐다.

 

지난번에 작성한 부분은 아래와 같고, 그것에 대한 소스코드는 여기로 들어가면 된다.

https://kimfish.co.kr/328

1
2
3
4
5
6
7
<MAIN>
  <AGE>16</AGE>
  <SEX>Male</SEX>
  <JOB>Student</JOB>
  <PHONE>01022223333</PHONE>
  <STATUS>Normal</STATUS>
</MAIN>
cs

여튼, 이번에 서버와 통신을 해보니 다음과 같은 형식으로 이루어져야만 했다.

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="EUC-KR" standalone="no"?>
<MAIN>
  <AGE value="16"/>
  <SEX value="Male"/>
  <JOB value="Student"/>
  <PHONE value="01011112222"/>
  <STATUS value="Normal"/>
</MAIN>
cs

허미 ==;

여튼 그래서 다음과 같이 작성했다. 지난번의 경우에는 jackson-dataformat-xml 라이브러리를 활용했으나, 이번에는 기본적인 DocumentBuilderFactory를 활용하는걸로..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.StringWriter;
 
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
 
import org.w3c.dom.Element
import org.w3c.dom.Document;
import org.w3c.dom.Attr;
 
 
@GetMapping(path="/Main", produces=MediaType.APPLICATION_XML_VALUE)
public String xmlTestMain(HttpServletRequest request) throws Exception{
        Document doc = null;
        DocumentBuilderFactory factory = null;
        factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        doc = builder.newDocument();
        
        //최상위 Root를 구성하는 Element 생성
        Element rootElement = doc.createElement("MAIN");
        doc.appendChild(rootElement); 
 
        //하위 Element를 생성. 
        Element ageElement = doc.createElement("AGE");
        //Attribute 를 신규로 선언해주고, Document를 이용하여 value를 생성함
        Attr attr = doc.createAttribute("value");
        //생성된 Attr에 value를 세팅
        attr.setValue("1");
        //생성한 ageElement에 위에서 신규선언된 Attr을 세팅
        ageElement.setAttributeNode(attr);
        // ageElement를 rootElement의 하위요소로 지정
        rootElement.appendChild(ageElement);
 
        //위의 과정을 아래와 같이 단순화 시킬 수 있음
        Element sexElement= doc.createElement("SEX");
        sexElement.setAttribute("value""Male");
        rootElement.appendChild(sexElement);
 
        Element jobElement = doc.createElement("JOB");
        jobElement .setAttribute("value""Student");
        rootElement.appendChild(jobElement );
 
        Element phoneElement = doc.createElement("PHONE");
        phoneElement .setAttribute("value""01011112222");
        rootElement.appendChild(phoneElement );
 
        Element statusElement = doc.createElement("STATUS");
        statusElement .setAttribute("value""Normal");
        rootElement.appendChild(statusElement );
 
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, "euc-kr");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        doc.setXmlStandalone(false);
 
        StringWriter sw = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(sw));
 
        String aaa = sw.getBuffer().toString();
        return aaa;
}
 
cs

에휴 일단 됐다... 좀 쉬자

'어장 Develop > 어장 JAVA' 카테고리의 다른 글

[Tomcat 9.x] JNDI설정  (0) 2022.02.21
[PDFBox] java Image to PDF  (0) 2021.08.24
[xml] RestAPI XML Return  (0) 2021.07.02
[SpringBoot] Async 사용하기  (0) 2021.06.25
[SpringBoot] fixed delay, fixed rate의 차이  (0) 2021.06.23
블로그 이미지

김생선

세상의 모든것을 어장관리

,

RestAPI 통신을 활용해서 개발하다보면 접속대상지 서버가 구성되는데까지 시간이 다소 소요되는 경우가 많다. 그래서 나의 경우에는 자체적으로 dummy rest api 서버를 만들어두고 더미데이터를 return 해주는 것으로 임시방편 해결을 하는 편.

이번엔 XML 형태의 RestAPI 통신이 이루어지다보니, 잘 다루지 않던 XML 파싱 및 XML Return하는 것을 간단히 정리한다.

 

필요한 라이브러리는 다음과 같으며, maven에서는 바로 가져올 수 있다.

1. jackson-dataformat-xml-2.11.4.jar
2. (의존성) jackson-core-2.11.4.jar
3. (의존성) woodstox-core.6.2.3.jar
4. (의존성) jackson-databind-2.12.3.jar
5. (의존성) stax-2-api-4.2.1.jar
6. (의존성) jakarta.activation-api-1.2.1.jar
7. (의존성) jakarta.xml.bin-api-2.3.2.jar
8. (의존성) jackson-module-jaxb-annotations-2.11.4.jar

 

만들고자 하는 XML 형태는 아래와 같다.

1
2
3
4
5
6
7
<MAIN>
  <AGE>16</AGE>
  <SEX>Male</SEX>
  <JOB>Student</JOB>
  <PHONE>01022223333</PHONE>
  <STATUS>Normal</STATUS>
</MAIN>
cs

 

소스는 다음과 같다.

 

1.  testXml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
@Data
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName="MAIN"//루트태그명
public class TESTXml {
  @JacksonXmlProperty private String AGE;
  @JacksonXmlProperty private String SEX;
  @JacksonXmlProperty private String JOB;
  @JacksonXmlProperty private String PHONE;
  @JacksonXmlProperty private String STATUS;
}
cs

xml로 생성할 항목들에 대해 TESTXml이라는 이름으로 데이터 클래스를 생성해준다.

@JacksonXmlProperty Annotation은 루트태그의 하위에 들어가는 그룹 요소로 생성이 된다.

 

2. DummyController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.http.MediaType;
 
 
@GetMapping(path="/xmlTest", produces=MediaType.APPLICATION_XML_VALUE)
public TESTXml xmlDummy(HttpServletRequest request) throws Exception {
  TESTXml testxml = new TESTXml();
  testxml.setAGE("16");
  testxml.setSEX("Male");
  testxml.setJOB("Student");
  testxml.setPHONE("01011112222");
  testxml.setSTATUS("Normal");
  
  return testxml;
}
cs

lombok 라이브러리를 이용했기에 getter/setter 지정이 아주 깔끔하다. 이렇게 해주면 /xmlTest라는 URL을 호출했을 때 예제와 같은 형식으로 사용이 가능하다.

 

아래의 블로그를 통해 정리했다. 도움 많이 받음.

https://binit.tistory.com/28

블로그 이미지

김생선

세상의 모든것을 어장관리

,

스프링부트에서 Async를 사용할 일이 생겼다. 대략적인 비즈니스 로직은 다음과 같다.

 

1. Scheduler를 통해 DB를 Select 함
2. 대략 1500~2000건의 데이터를 A 서버에 HttpConnection을 이용하여 Request/Response를 받아야 함
3. 최대한 빨리

일단 가장 심플하게 생각하자면 @Scheduled를 이용, DB를 쿼리한 후 해당 List를 for문으로 돌리면서 A서버로 httpConnection을 이용하면 될 것 같고 실제로 그렇게 구현했다. 하지만 Request/Response의 응답시간이 건당 약 3초가 소요되며, 전체적으로 건수가 많기에 1500 * 3 = 4500초가 걸리는 초유의 사태가 발생했다. 그래서 결국엔 Async를 이용해 전체로직을 다 뜯어고치게 되었다. 내게 이렇게 오래걸리면 안된다고 왜 미리 말을 안해준거니. 하루에 두 번만 수행하면 된다매!

 

기본적으로 다음과 같이 소스를 구성했다.

1. Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.scheduling.annotation.Scheduled;
 
// 중략
@Autowired
HttpConnectorUtil httpConnUtil;
 
@Scheduled(fixedDelay = 300000)
public void dbSelectScheduler() throws InterruptedException {
  logger.info("dbSelectScheduler Start!");
 
  JSONObject queryObj = new JSONObject();
  try {
    // DB 쿼리부. 대충 1000개의 사이즈를 가진 List라고 치자
    List<HashMap<String, Object>> targetList = dbService.selectTargetQuery(queryObj);
 
    for ( int a = 0 ; a < targetList.size(); a++){
      Thread.sleep(300); //너무 빨리 요청하면 서버가 못버팀
      httpConnUtil.HttpConnection("http://localhost:8080/localTest", targetList.get(a).toString());
    }
 
  } catch {
    e.printStackTrace();
    logger.info("ReqService ERROR! {}" , e.getMessage());
  }
 
  logger.info("dbSelectScheduler End!");
}
cs

여기에서는 별다른 로직이 없다. DB에서 쿼리한 갯수만큼 for문을 돌리고, for 내부에서는 지정된 URL에 대해 httpConnector 요청을 날린다.

 

2. AsyncConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.Executor;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
//Configuration 선언 필수
@Configuration
// EnableAsync 선언 필수. 여기서 해당 플젝의 AsyncConfigurer를 공통선언/사용
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
 
  @Override
  public Executor getAsyncExecutor(){
    ThreadPoolTaskExecutor asyncExecutor = new ThreadPoolTaskExecutor();
    asyncExecutor.setCorePoolSize(10); //최소 동작하는 Async Thread 수
    asyncExecutor.setMaxPoolSize(20);  //최대 동작하는 Async Thread 수
    asyncExecutor.setQueueCapacity(100); // 최대 동작 Thread를 초과할 시, 보관되는 Queue의 수
    asyncExecutor.setThreadNamePrefix("TESTAsync-");  // Async Thread가 동작할 때 표시되는 Thread Name
    asyncExecutor.initialize();  //위 설정 적용
    return asyncExecutor;
  }
}
cs

현재 프로젝트에서 공통적으로 사용하는 AsyncConfig이다. 보다 자세한 내용은 아래의 블로그에서 확인이 가능하다. 해당 블로그를 통해 많이 배웠다. https://www.hanumoka.net/2020/07/02/springBoot-20200702-sringboot-async-service/

 

springboot 비동기 서비스 만들기(Async)

들어가기springboot rest 서버에서 어떤 요청을 받으면, Shell command를 호출하는 기능을 구현해야 한다. 문제는 Shell command로 호출하는 호출하는 것이 Python 스크립트이고, 이 스크립트 동작이 몇분은

www.hanumoka.net

여튼, 기본적으로 숙지해야 할 사항은 이번 플젝은 언제든 Thread의 수가 변할 수 있음을 감안하고 setCorePoolSize, setMaxPoolSize는 application.yml에서 가져올 수 있도록 수정했다. 현재는 위 설정대로 1500건에 대해 300ms의 thread.sleep을 준 상태로 요청을 하며, 아직까지는 문제없이 2500건까지 수행하였다.

 

@EnableAsync를 통해 프로젝트에서 공통적으로 사용할 AsyncExecutor를 설정한다. 이는 다른 AsyncService에서도 사용하므로 자신의 프로젝트 상황을 고려하여 설정값을 변경하면 된다.

 

3. AsyncService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.scheduling.annotation.Async;
//다른 import들은 생략
 
// Service 에서만 Async 어노테이션이 정상동
@Service
public class AsyncService {
 
  //Async를 사용할 때에는 아래의 @Async Annotation을 선언
  @Async
  public JSONObject HttpConnection(String url , String strJson) throws Exception{
    //대충 httpconnection에 필요한것들 선언, 생략
 
    try {
       URL url = new URL(url);
       HttpURLConnection conn = (HttpURLConnection) url.openConnection();
       //conn에 해당하는 내용 생략
       
       OutputStream = os = conn.getOutputStream();
       os.write(strJSON.getBytes("UTF-8"));
       os.flush();
 
       //리턴내용 생략
    } catch (Exception e) {
      e.getMessage();
    }
 
    return jsonResult;
  }
 
}
cs

 

실질적으로 Async요청을 하는 부분이다. controller에서는 AsyncService(@Service)를 호출하여, @Async 로 선언된 HttpConnection을 사용하게 된다. 이로써 Http를 호출하는 부분은 Async로 동작하게 된다.

 

실행 로그는 다음과 같다.

1
2
3
4
5
6
2021-06-21 16:39:41.300 INFO 2068 --- [  TEST-Async-3] com.example.AsyncService: [HttpConnectorResult] 1
2021-06-21 16:39:41.400 INFO 2068 --- [  TEST-Async-1] com.example.AsyncService : [HttpConnectorResult] 22
2021-06-21 16:39:41.510 INFO 2068 --- [  TEST-Async-2] com.example.AsyncService : [HttpConnectorResult] 4
2021-06-21 16:39:41.997 INFO 2068 --- [ TEST-Async-10] com.example.AsyncService : [HttpConnectorResult] 11
2021-06-21 16:39:42.010 INFO 2068 --- [  TEST-Async-7] com.example.AsyncService : [HttpConnectorResult] 3331
2021-06-21 16:39:42.314 INFO 2068 --- [  TEST-Async-5] com.example.AsyncService : [HttpConnectorResult] 51
cs

처음에는 AsyncConfig의 설정 방법 등을 몰라 @Async를 선언해준 class에서 위의 executor를 죄다 세팅해주었는데, 이 포스트를 작성하면서 샘플소스를 만들어보니 공통으로 사용가능하단 점을 깨닫게 되었다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

지금껏 플젝을 진행하면서 SpringBoot의 cron Scheduler만 사용을 하다가, 특이한 요구사항(?)으로 인해 fixedDelay, fixedRate를 사용하게 되었다. 사용을 해보니 fixedDelay만을 사용하게 되었지만, 이 참에 정리해보고자 한다.

 

일단 fixedDelay와 fixedRate의 구현은 다음과 같이 진행한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.scheduling.annotation.Scheduled;
 
@Scheduled(fixedDelay = 1000)
public void fixedDelayScheduler() {
  logger.info("FixedDelay Start!");
  try {
    Thread.sleep(600);
  } catch {
    e.printStackTrace();
  }
  logger.info("FixedDelay End!");
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.scheduling.annotation.Scheduled;
 
@Scheduled(fixedRate = 1000)
public void fixedRateScheduler() {
  logger.info("FixedRate Start!");
  try {
    Thread.sleep(600);
  } catch {
    e.printStackTrace();
  }
  logger.info("FixedRate End!");
}
cs

사실 구현에 있어 fixedDelay , fixedRate의 차이는 존재하지 않는다. 다만, 어떻게 동작하느냐가 매우 다르므로 사용상에 유의할 필요가 있다.

 

먼저, fixedDelay는 현재 Schedule 상에 걸린 작업을 모두 끝난 이후에 설정된 시간이 카운팅되는 형태이며

fixedRate는 현재 Schedule 상에 걸린 작업의 완료 여부와 상관 없이 Scheduler가 시작한 시간으로부터 카운팅되는 형태이다.

 

위의 소스에 대해 로그를 살펴보자.

1
2
3
4
5
6
7
### Fixed Delay ###
2021-06-21 14:05:36.094  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:36.695  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
2021-06-21 14:05:37.698  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:38.300  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
2021-06-21 14:05:39.301  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay Start!
2021-06-21 14:05:39.903  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedDelay END!
cs

 

1
2
3
4
5
6
7
### Fixed Rate ###
2021-06-21 14:06:42.762  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:43.363  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
2021-06-21 14:06:43.763  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:44.365  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
2021-06-21 14:06:44.765  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate Start!
2021-06-21 14:06:45.367  INFO 5536 --- [   DUMMY-1] com.example.DummyController   : FixedRate END!
cs

두 소스 모두 Start와 End 사이에 600ms의 Sleep을 주었다. 그 후에 End log가 찍히며, 이후 설정된 시간인 1000ms(1sec) 이후에 스케쥴러가 재실행되는 구조로 되어있다.

 

FixedDelay의 경우에는 14:05:36.094에 Start, 600ms Sleep 이후인 14:05:36:695에 End가 찍혔으며 End log가 찍힌 1000ms 이후인 14:05:37:698에 해당 스케쥴러가 실행되었다. 첫번째 Start 로그와 두번째 Start 로그의 시간차는 중간에 Thread.sleep으로 걸어준 600ms가 반영된 1600ms 만큼 차이가 존재한다.

 

FixedRate의 경우에는 14:06:42.762에 Start, 600ms Sleep 이후인 14:06:43:363에 End가 찍혔으나 그 시간과는 상관없이 Start 로그는 첫번째 Start 로그가 찍힌 시간에서 1000ms 이후인 14:06:43:763에 Start 로그가 찍혔다. 즉, 첫번째 Start 로그와 두번째 Start 로그의 시간차는 도중의 Thread.Sleep이 무시가 된 1000ms로 동일하다.

 

이는 스케쥴러 작업에 아주 중요한 영향을 끼친다. 내가 이번 프로젝트에서 fixedRate를 배제하고 fixedDelay를 사용하게 된 이유는 다음과 같다.

 

조건1. A 스케쥴러는 5분(300초)의 주기로 작동한다
조건2. A 스케쥴러에서 수행하는 작업은 짧게는 10초, 길게는 10분 이상이 소요된다
조건3. 조건2의 작업에서, DB의 Insert/Update 작업이 수시로 발생한다

위와 같은 작업상황에서 fixedRate로 수행을 할 경우, 조건 1의 10분정도 걸리는 작업을 수행하는 경우, 5분이 지난 시점에 동일한 A 스케쥴러가 작동을 하게 될 것이며, 같은 작업을 두 번 이상 수행하는 결과를 낳게 된다.

이는 의도된 행동과는 다르게 동작하기에, 동일한 데이터에 대해 한 번만 처리가 되어야 하는 작업을 여러번 처리하게 되므로 오작동을 하게 된다.

 

이에, 수행시간동안에는 스케쥴러가 동작하지 않고, 작업이 끝난 이후 설정된 시간 텀을 주는 fixedDelay를 사용하게 된 이유라고 볼 수 있다.

 

추가로, fixedDelayString , fixedRateString으로도 선언이 가능하며, 이는 long형 대신 String 형으로 선언을 받는다는 차이가 있다. application.yml에 선언하고 쓰기에 좋음ㅋ

 

*** 하나 더 추가

Springboot 에서 @Scheduled 을 사용하기 위해서는 메인 클래스에 @EnableScheduling 을 선언해주어야 정상 동작한다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

SpringBoot로 프로젝트를 진행하면서 앞으로 쓰일 여러가지들을 정리해본다.

 

1. 단일 항목 가져오기

application.yml과 application.property는 문법이 조금 다르다지만 뭐 그런건 크게 중요하지 않다.

일단 단일 항목을 가져오는 방법에 대해 서술한다.

 

1
2
3
4
5
### Saimple application.yml ###
 
async:
 coreName: Test
 coreSize: 10
cs

 

1
2
3
4
5
6
7
import org.springframework.beans.factory.annotation.Value;
 
@Value("${async.coreName}")
private String asyncCoreName;
 
@Value("${async.coreSize}")
private int asyncCoreSize;
cs

요러케 하면 String/Int 형으로 각각의 선언된 항목을 가져올 수 있다.

 

2. Array 항목 가져오기

1
2
3
4
### Saimple application.yml ###
 
async:
 intervalTime: 0910111213
cs
 
1
2
3
4
5
6
7
8
import org.springframework.beans.factory.annotation.Value;
 
@Value("${async.intervalTime}")
private String[] strAsyncIntervalTime;
 
@Value("${async.intervalTime}")
private int[] intAsyncIntervalTime;
 
cs

 

Array로 지정된 항목들에 대해서는 sample application.yml과 같이 쉼표, 공백문자열을 이용해 구분해준다.

String/Int 형태의 Array로 변환이 가능하다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

중계서버를 만드는데, 내부에서 Postman으로 쏠땐 잘 되고, 타 팀의 개발자가 피들러로 쏠 땐 500 에러가 떨어진다고 메시지를 보내왔다. 뭐가문젠가? 이상하다? 그래서 로그를 찍어봤다.

 

현황을 보니 일단 bearer token도 잘 들어있고, 빈 파라미터도 없이 문제는 크게 보이지 않는다. 심지어, 이중구조로 된 곳에서 데이터는 잘 들어왔고 DB에 insert까지 잘 된 상황. 그런데 response에서 에러가 발생하는거다. 뭔가 이상하다. 그래서 header를 보기 시작했다.

 

기본적으로 삽입되는 header 외에, 타 팀에서 삽입한 header에 못보던 놈이 들어가있었다. "Expect": "100-continue"항목이었다. 이걸 내 Postman 에서 삽입해보고 테스트하니 동일한 500 에러가 발생한다. 해당 항목을 빼고 테스트를 요청하자, 정상적으로 response가 동작했다.

 

그래서 저게 무슨 옵션인가, 하고 찾아보니 대략적으로 클라이언트가 서버의 상태를 보고 request를 요청하는데 서버가 http 1.1을 지원하지 않는 경우에는 오류가 발생한다 적혀있다. 내 플젝은 http/1.1을 지원하지만, 앞단에 있는 gateway 서버에서 해당 옵션을 지원하지 않는것으로 확인. 뭐 이렇게 처리했다.

 

별 내용은 아니지만 혹시나 나중에 찾을까봐.

'어장 Develop' 카테고리의 다른 글

[Mac] chromedriver 설치부터  (1) 2022.04.10
[MacBook] M1 MacBookPro 에 openjdk설치  (0) 2022.04.09
[tomcat] p12 ssl 적용  (0) 2020.06.08
[Linux] Centos Alias 설정  (0) 2020.04.11
[Linux] make: g++: Command not found  (0) 2019.08.12
블로그 이미지

김생선

세상의 모든것을 어장관리

,

0. 현재상황

중계서버를 만들고 있다. 개발하다 보면 흔히 있는 일인데, 앞단(통칭 A)에서 전달해주는 JSON Key와 우리가 전달해줘야 하는 뒷단(통칭 B)의 JSON Key가 다른 경우가 있다. 같은 의미의 파라미터라고 하더라도, A서버는 "testEMail" 이라는 Key로 전달해줄 때, B서버는 "test_email"이라는 key로 받아야 한다. 그럴 땐 대충 아래와 같이 작업하는 편이다.

1
2
3
4
5
Map<String, Object> bMap = new HashMap<String, Object>();
//bMap에 담아서 B서버로 전달
if ( jsonResult.containsKey("testEMail")) {
   bMap.put("test_email", jsonResult.get("testEMail"));              
}
cs

뭐 근데 쓰다보면 불편한 점이 참 많다. 간단하게 한두개야 이렇게 한다손 치더라도, 후에는 오류가 생길 수 있다. 담당 개발자가 바뀐다거나 또는 Parameter Key가 변경된다거나 하는 정도로. 그래서 이래저래 고민하다 찾아보니 Enum이 있더라. 그래서 냅다 바꿨다.

 

1. Enum class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum aParamMapper {
    //좌측 - a서버 , 우측 - b서버
    testNm("testNm""test_nm"),    //사용자명
    testTelNo("testTelNo""test_tel_no"), //사용자전화번호
    testEMail("testEMail""test_email"), //사용자이메일
    ;
 
    private String aParam;
    private String bParam;
 
    bParamMapper(String aParam, String bParam) {
        this.aParam = aParam;
        this.bParam = bParam;
    }
}
 
cs

enum class를 하나 만들어서, 4~6번 라인과 같이 매칭시켜줄 것들을 만든다. 타입에는 arraylist도 들어가고 많은데, 아직 그런 활용성은 생기지 않았다.

 

2. 사용

1
2
3
4
5
6
7
8
Map<String, Object> bMap = new HashMap<String, Object>();
//bMap에 담아서 B서버로 전달
if ( jsonResult.containsKey(bParamMapper.testNm.getbParam())) {
   bMap.put(
       bParamMapper.testNm.getbParam()
       , jsonResult.get(bParamMapper.testNm.getaParam())
        );
}
cs

* 소스가 좀  길어져서 임의로 줄바꿈 함

0. 현재상황 의 4번 라인은 key/value 값이 각각 String 인자값으로 하드코딩 되어있는데 반해, 2.사용 의 5~6번 라인은 key/value 값을 enum을 통해 가져오는 방식으로 바뀌었다.

한두군데에서 쓰는 방식이라면 굳이 enum을 써야하나 싶긴 한데, 이를 활용하면 공통되는 부분을 모두 대체할 수 있는 데다가, 향후 Parameter Key가 변경되었을 때의 대처방안, 대소문자 구분자에 대한 해결방안 등도 모두 대응할 수 있을 것으로 생각된다. 

블로그 이미지

김생선

세상의 모든것을 어장관리

,

고객사에서 MariaDB를 설치하고 있다. 그러던 와중, 잘 구동되던 Tomcat을 재구동하게 되었는데 오류메시지 "Access denied for user '계정명'@'localhost'(using password:YES)" 라는 오류가 발생한다. 이전까지는 재구동하는데 문제가 없었고, 오류가 생길만한 작업은 약 서너가지 정도가 짚히는 상황이었다. 현재 WAS 설정으로는, server.xml 및 context.xml 내에서 JNDI를 해당 MariaDB로 설정해둔 상태였다.


먼저, MariaDB에 익숙하지 않은 나인지라 용어의 혼란이 조금 있을 것 같아 아래와 같은 내용으로 이해를 하고 있다.  MariaDB는 계정별로 두 개의 권한을 갖는 듯한 모습이었다. MariaDB의 계정 테이블을 쿼리해보면 Host라고 작성된 부분이 있는데, 여기에 localhost 및 % 정도로 권한(?)이 부여가 된다. localhost는 내부에서만 접속이 가능하고, %은 내/외부에서 접속이 모두 가능한 권한 정도로 이해하고 있다.


1. 원인파악

본론으로 돌아와서 검색해보니 뭐 당연하게도 해당 계정의 localhost 권한이 없다는 문구다. 그래서 MariaDB가 설치된 서버에 접속하여 아래의 명령어를 사용했다. 해당 명령어는 shell 에서 MariaDB에 접속하기 위한 명령어이다.


1
mysql -u 계정명 -p
cs

그런데 위의 tomcat과 마찬가지로 "Access denied for user '계정명'@'localhost'(using password:YES)" 오류가 발생한다. 그래서 MariaDB의 root계정으로 접속하려는데, root 권한 또한 마찬가지 오류가 발생한다. 


이래저래 구글링을 해보니, 다음과 같은 명령어를 찾게 되었다.

1
sudo /usr/bin/mysqladmin -u root password [비밀번호]
cs

해당 명령어는 서버의 root 권한으로 서버 내에 설치된 MariaDB의 root 비밀번호를 재설정하는 것이다. 이 이후에, mysql -u root -p 명령어로 접속하였고, localhost 계정을 다시 생성해주려 하였다.


1
MariaDB[(none)]> CREATE USER '계정명'@'localhost' IDENTIFIED BY '비밀번호';
cs

했더니, 아래와 같은 오류메시지가 출력된다.


1
ERROR 1396(HY000) : Operation CREATE USER failed for '계정명'@'localhost';
cs

뭔가 해서 봤더니 직접 생성이 불가능한 것 같다. 혹시나 싶어, 계정 테이블을 직접 쿼리해서 상태를 좀 보고자 했다.


1
2
MariaDB[(none)] > use sql; --Database를 변경하는 명령어
MariaDB[(mysql)] > SELECT * FROM user WHERE user='계정명';
cs

해당 명령어로 조회해보니 우리 DB에서 사용중인 계정 - Host(권한이라고 이해중)별 비밀번호가 각각 다른것이었다. A 계정의 % Host(권한)과 A 계정의 localhost Host(권한)과 비밀번호가 각기 다른 상황. 이러니 서버나 tomcat에서 접속할 때에는 Access Denied For User 에러가 발생했을 것이고, 개발자 PC의 DB툴로는 접속이 잘 되었겠지. 이제 원인이 파악된 상황이다.


1
2
3
4
5
6
7
8
9
10
+-------------------------------------------------------------------------------+
| Host      | User        | Password              | Select_priv  | Insert_priv  |
+-------------------------------------------------------------------------------+
| localhost | mariadb.sys |                       | N            | N            |
| localhost | root        | *E1C459C5 ~~~~~~~~~   | Y            | Y            |
| localhost | mysql       | invalid               | Y            | Y            |
| localhost |             |                       | N            | N            |
| %         | test        | *0B26A ~~~~~~~~~~~~~~~| N            | N            | 
| localhost | test        | *COB5F ~~~~~~~~~~~~~~~| N            | N            | 
+-------------------------------------------------------------------------------+

cs

* test라는 계정의 % Host 비밀번호는 암호화되어 *0B26A로 시작하는데, localhost는 *COB5F로 시작한다. 이러니 외부에서 접속하는 % Host는 기존 비밀번호로 접속이 되고, 같은 서버 내에 있는 tomcat은 기존 비밀번호로 접속이 안되겠지.



2. 문제해결


일단 원인은 알았다. test 계정의 % 비밀번호와 test 계정의 localhost 비밀번호가 다르기에 생긴 문제라는 것. 일단 localhost 비밀번호가 왜 저렇게 변경되었는지는 작업자들에게 물어보면 될 것이고, 접속이 되도록 수정을 해야겠다. 이럴땐 뭐다? test 계정의 localhost Host(권한)비밀번호를 기존에 사용중인 비밀번호로 Update 쳐주면 될 것이다.


1
MariaDB[(mysql)] > UPDATE user SET password=('비밀번호') WHERE user='계정명';
cs

했더니, 다음과 같은 에러가 발생한다.


1
ERROR 1348(HY000) : Column 'Password' is not updatable.
cs

않이; 뭐냐 대체 이건. 또다시 검색해보니 MariaDB 10.4 버전 이상은 user테이블을 직접관리하지 않고, 파일이나 뭐 그런식으로 관리하기 때문에 기존의 user테이블 update 방식은 사용할 수 없다고 한다. 그래서 또다시 뒤져보니 명령어로 직접세팅하는 방법이 있었다.


1
MariaDB[(mysql)] > set password for '계정명'@'localhost=password('비밀번호');
cs

했더니, Query OK, 0 rows affected (0.014sec) 메시지가 출력되었다. 그리고 다시 user 테이블을 확인해보았고, 해당 계정의 % Host와 localhost Host의 비밀번호가 동일한 것을 확인할 수 있었다.


1
2
3
4
5
6
7
8
9
10
+-------------------------------------------------------------------------------+
| Host      | User        | Password              | Select_priv  | Insert_priv  |
+-------------------------------------------------------------------------------+
| localhost | mariadb.sys |                       | N            | N            |
| localhost | root        | *E1C459C5 ~~~~~~~~~   | Y            | Y            |
| localhost | mysql       | invalid               | Y            | Y            |
| localhost |             |                       | N            | N            |
| %         | test        | *0B26A ~~~~~~~~~~~~~~~| N            | N            | 
| localhost | test        | *0B26A ~~~~~~~~~~~~~~~| N            | N            | 
+-------------------------------------------------------------------------------+

cs

이후, tomcat을 재구동하였더니 정상동작 하였다.



3. 후기


MariaDB를 직접적으로 설정하고 사용할 일이 없어 여러모로 삽질을 많이하게 되었다. 문제해결하면서 알게된 점을 아래에 간략하게 작성해보았다.

계정은 Host(권한)별로 password가 지정된다는 점이다. 위에서도 보여지듯, test 계정임에도 불구하고 로컬(같은 서버내)에서 접속가능한 비밀번호와 외부에서 접속 가능한 비밀번호가 각기 설정되어있다.


또한, 명령어의 password('비밀번호')는 자동으로(?) 인코딩되어 설정된다는 점이었다. 혹시나 싶어 구글링한 결과 그대로 작성해보았는데, 알아서 인코딩되어 저장되는 것을 보니 어딘가에 설정이 박혀있는게 아닐까 싶다. 뭐 복잡시럽게 sha256으로 인코딩해서 직접 박아넣는게 아니라 다행이다 싶다.


위의 상황이 발생하게 된 계기는, 하나의 서버를 여러부서가 공동으로 개발장비로 사용하는 중이었고, MariaDB를 우리팀에서 설치, 사용을 하고 있었다. 그러다가 타 부서에서 MariaDB를 사용 할 일이 생기게 되었고, 우리가 설치한 MariaDB에 접속이 안되니 멋대로 서버의 root 권한으로 test계정의 localhost 비밀번호를 바꿔버려 생긴 문제였다. 거기에, 우리팀 막내가 당일 MariaDB 설정과 관련하여 파일 권한이라거나 기타 설정들을 바꾸면서 MariaDB Service를 재구동하는 등 여러 변경점이 생기다보니 우리팀 내에서만 원인 찾기가 힘든 상황.


나의 경우에는 계정-host별 비밀번호가 다르게 적용되는 줄 몰랐던터라, 애꿎은 DB보안프로그램과 서버접속 보안프로그램 탓을 했었다. tomcat 재구동 작업을 하기 바로 전날 야간에 설치가 되었던터라, 이 부분이 문제인줄 알았던 것. 여러 작업적인 이슈가 겹치다보니 생긴 당연한 인재였다. 개발서버였기에 망정이지, 실 운영이 진행중인 운영서버였다면 초대형사고였을지도 모른다. 이러한 이유로, 무슨 작업이든 작업이 있을 경우에는 연관부서에 모두 noti를 해줘야한다는 것이다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,

프로젝트를 하다가 p12 key로 SSL 적용할 일이 생겼다. 지금까지는 pem 키라거나 뭐 그런거로 했었는데. 여튼 오랜만에 적용해보니 살짝 헤매서 정리해놓는다.


p12든 뭐든 사실 크게 다른 건 없다. 단지 $catalina_home/conf/server.xml이 조금 달라질 뿐.


1
2
3
4
<Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol"
  maxthreads="150" SSLEnabled="true" scheme="https" secure="true"
  clientauth="false" sslProtocol="TLS" keystoreType="pkcs12" 
  keystorePass="비밀번호" keystoreFile="/home/keystore/keystore.p12"/>
cs


별다른 건 없다. 위와 같이 적용해주면 된다.

기존의 Connector는 주석으로 너굴맨이 처리했으니 걱정말라구!


주의할 점은 keystoreType이 대소문자를 구분하는건지, PKCS12로 썻다가 안먹혀서 좀 당황했다.

블로그 이미지

김생선

세상의 모든것을 어장관리

,