728x90
반응형

Java 기반의 프로그램을 개발하기 전에 앞서서 필수적으로 설치해야하는 패키지가 있다.
바로 JDK이다.

JDK는 Java Development Kit의 약자로서, Java 기반 프로그램 개발에 필요한 필수적인 요소들이 담긴 패키지라고 할 수 있다.

JDK를 설명하라고 한다면, 위 사진 하나 업로드 해놓고 설명을 끝낼 수 있을 정도로 명확하다.
그래도 하나씩 살펴보자.

JVM

JVM은 Java Virtual Machine의 약자이다.
Class 파일 형식의 Java 프로그램을 실행시켜주는 하나의 프로그램이다.

여기서 두 가지가 궁금해질 것이다.

첫번째는 JVM이 어떻게 Class 파일을 읽어서 Java 프로그램을 구동시켜주는지에 대한 부분이다.
해당 부분은 👉다음 연재될 글에서 살펴볼 것이다.

두번째는 과연 Class파일이 뭔지에 대해 궁금해질 것이다.

Class파일이란? JVM이 읽을 수 있도록 Java 언어로 구성된 코드를 컴파일하여 생성한 파일이다.
해당 Class파일은 Java 컴파일러가 만들 수 있고, Java 코드 내 Class 단위마다 파일 하나씩 바이트코드로 생성한다.

JRE

JRE는 Java Runtime Environment의 약자이다.
JVM의 상위 개념인데, JVM과 Java 필수 라이브러리(ex. java.lang, java.util 등)로 구성되어 있다.


JDK

JDK는 앞서 언급한 것처럼 Java 개발에 필수적인 요소가 포함되어 있다.
하위개념인 JRE과 개발에 필요한 Java컴파일러, Java디버거, Java Heap 등과 같은 Tool을 종합하여 패키징한 것이 JDK이다.

그래서 Java 개발환경을 세팅한다고 하면,
JDK와 IDE (ex. Eclipse, IntelliJ) 두 가지를 설치하고 IDE에 Java 프로젝트를 생성한 후에 JDK를 연결하는 일련의 과정을 거치면 된다.

반응형
728x90
반응형

SQLD 시험 준비

  • 모델링의 정의
  1. 정보시스템을 구축하기 위한 데이터 관점의 업무 분석 기법
  2. 현실세계의 데이터에 대해 약속된 표기법에 의해 표현하는 과정
  3. 데이터베이스를 구축하기 위한 분석/설계의 과정

  • 모델링의 특징
  1. 추상화 : 현실세계를 일정한 형식에 맞추어 표현
  2. 단순화 : 복잡한 현실세계를 약속된 규약에 의해 제한된 표기법이나 언어로 표현하여 쉽게 이해할 수 있도록 하는 개념
  3. 명확화 : 누구나 이해하기 쉽게 하기 위해 대상에 대한 애매모호함을 제거하고 정확하게 현상을 기술

  • 모델링의 관점
  1. 데이터 관점 : 업무가 어떤 데이터와 관련이 있는지 또는 데이터 간의 관계는 무엇인지?
  2. 프로세스 관점 : 실제하고 있는 업무는 무엇인지 또는 무엇을 해야하는지?
  3. 데이터와 프로세스의 상관 관점 : 업무가 처리하는 일의 방법에 따라 데이터는 어떻게 영향을 받고 있는지?

  • 모델링이 중요한 이유
  1. 파급효과 : 데이터 구조 변경 시, 영향도가 큼
  2. 복잡한 정보 요구 사항의 간결한 표현 : 데이터 모델은 건축물로 비유하자면 설계 도면
  3. 데이터 품질 : 아래는 유의할 점
    1. 중복 : 여러 장소에 같은 정보 저장 x
    2. 비유연성 : 데이터의 정의를 데이터의 사용 프로세스와 분리함으로서 영향도 줄임
    3. 비일관성 : ex. 신용상태에 대한 갱신 없이 고객의 납부 이력 정보를 갱신 => 납부이력정보와 신용상태는 동시에 갱신되어야 함

  • 데이터 모델링의 3단계 : 아래로 내려갈수록 구체적, 위로 올라갈수록 추상적 => 보통 아래 순서대로 데이터 모델링이 진행됨
  1. 개념적 데이터 모델링 : 추상화 수준이 높고 업무 중심적이고 포괄적인 수준의 모델링 진행. 전사적 데이터 모델링, EA 수립 시 많이 이용
  2. 논리적 데이터 모델링 : 시스템으로 구축하고자 하는 업무에 대해 Key, 속성, 관계 등을 정확하게 표현, 재사용성이 높음
  3. 물리적 데이터 모델링 : 실제로 데이터베이스에 이식할 수 있도록 성능, 저장 등 물리적인 성격을 고려하여 설계

  • 데이터베이스 3단계 구조 : 화면과 데이터베이스 간의 독립성 유지를 위함
  1. 외부 스키마 : 개개 사용자 단계로서 개개 사용자가 보는 개인적 DB 스키마
  2. 개념 스키마 : 모든 사용자 관점을 통합한 조직 전체의 DB를 기술하는 것
  3. 내부 스키마 : 물리적 장치에서 데이터가 실제적으로 저장되는 방법을 표현하는 스키마

  • 데이터 모델링의 세 가지 요소
  1. 업무가 관여하는 어떤 것 (Thins)
  2. 어떤 것이 가지는 성격 (Attributes)
  3. 업무가 관여하는 어떤 것 간의 관계 (Relationships)

  • ERD 작업순서
  1. 엔터티를 그린다.
  2. 엔터티를 적절하게 배치한다.
  3. 엔터티 간 관계를 설정한다.
  4. 관계명을 기술한다.
  5. 관계의 참여도를 기술한다.
  6. 관계의 필수여부를 기술한다.

  • 엔터티의 특징
  1. 반드시 해당 업무에서 필요하고 관리하고자 하는 정보이어야 한다.
  2. 유일한 식별자에 의해 식별이 가능해야 한다.
  3. 영속적으로 존재하는 인스턴스의 집합이어야 한다.
  4. 엔터티는 업무 프로세스에 의해 이용되어야 한다.
  5. 엔터티는 반드시 속성이 있어야 한다.
  6. 엔터티는 다른 엔터티와 최소 한 개 이상의 관계가 있어야 한다.

  • 엔터티의 관계가 필요 없는 경우
  1. 통계를 위한 엔터티
  2. 코드를 위한 엔터티
  3. 시스템 처리 시 내부 필요에 의한 엔터티

  • 엔터티의 분류
  1. 유무형에 따른 분류
    1. 유형엔터티 : 물리적인 형태가 있고 안정적이며 지속적으로 활용되는 엔터티 (ex. 사원, 물품, 강사)
    2. 개념엔터티 : 물티적인 형태가 없지만 관리해야 할 개념정 정보로 구분되는 엔터티 (ex. 조직, 보험상품)
    3. 사건엔터티 : 업무를 수행함에 따라 발생하는 엔터티 (ex. 주문, 청구, 미납)
  2. 발생시점에 따른 분류
    1. 기본엔터티 : 다른 엔터티와의 관계에 의한 것이 아닌 독립적으로 생성이 가능하고 타 엔터티의 부모 역할이 되는 엔터티 (ex. 사원, 부서, 고객, 상품, 자재)
    2. 중심엔터티 : 기본엔터티로부터 발생되고 업무에서 중심적인 역할을 하는 엔터티 (ex. 계약, 사고, 예금원장, 청구, 주문, 매출)
    3. 행위엔터티 : 두 개 이상의 부모엔터티로부터 발생하고, 자주 내용이 바뀌거나 데이터 양이 증가하는 엔터티 (ex. 주문목록, 사원변경이력)

  • 속성 : 업무에서 필요로 하는 인스턴스에서 관리하고자 하는 의미상 더이상 분리되지 않는 최소의 데이터 단위

  • 속성의 특징

  1. 반드시 해당 업무에서 필요하고 관리하고자 하는 정보여야 한다.
  2. 정규화 이론에 근거하여 정해진 주식별자에 함수적 종속성을 가져야 한다.
  3. 하나의 속성은 한 개의 값만을 가진다.

  • 속성의 특성에 따른 분류
  1. 기본속성 : 업무로부터 추출한 모든 속성
  2. 설계속성 : 업무상 필요한 데이터 이외에 데이터 모델링 및 업무를 규칙화하기 위해 새로 만들거나 변형한 속성 (ex. 일련번호)
  3. 파생속성 : 다른 속성에 영향을 받아 발생하는 속성, 보통 계산된 값들이 이에 해당, 가급적 적게 정의하는 것이 좋음.

  • 관계 정의 시, 체크할 사항
  1. 두 개의 엔터티 사이에 관심 있는 연관규칙이 존재하는가?
  2. 두 개의 엔터티 사이에 정보의 조합이 발생하는가?
  3. 업무기술서, 장표에 관계연결에 대한 규칙이 서술되어 있는가?
  4. 업무기술서, 장표에 관계연결을 가능하게 하는 동사가 있는가?

  • 식별자의 특징
  1. 유일성 : 주식별자에 의해 엔터티 내에 모든 인스턴스들이 유일하게 구분되어야 한다.
  2. 최소성 : 주식별자를 구성하는 속성의 수는 유일성을 만족하는 최소의 수가 되어야 한다.
  3. 불변성 : 지정된 주식별자의 값은 자주 변하지 않는 것이어야 한다.
  4. 존재성 : 주식별자가 지정이 되면 반드시 값이 들어와야 한다.

  • 식별자 분류
  1. 대표성 여부
    1. 주식별자 : 엔터티 내에서 각 어커런스를 구분할 수 있고, 타 엔터티와 참조관계를 연결할 수 있는 식별자
    2. 보조식별자 : 엔터티 내에서 각 어커런스를 구분할 수 있지만, 대표성을 가지지 못해 참조관계 연결을 못하는 식별자
  2. 스스로 생성 여부
    1. 내부식별자 : 엔터티 내부에서 스스로 만들어지는 식별자
    2. 외부식별자 : 타 엔터티와의 관계를 통해 타 엔터티로부터 받아오는 식별자
  3. 속성 수
    1. 단일식별자 : 하나의 속성으로 구성된 식별자
    2. 복합식별자 : 둘 이상의 속성으로 구성된 식별자
  4. 대체 여부
    1. 본질식별자 : 업무에 의해 만들어지는 식별자
    2. 인조식별자 : 업무적으로 만들어지지는 않지만 원조식별자가 복잡한 구성을 갖고 있기 때문에 인위적으로 만든 식별자

  • 주식별자 도출 기준
  1. 해당 업무에서 자주 이용되는 속성을 주식별자로 지정
  2. 명칭, 내역 등과 같이 이름으로 기술되는 것들은 가능하면 주식별자로 지정하지 않는다.
  3. 복합으로 주식별자로 구성할 경우 너무 많은 속성이 포함되지 않도록 한다.

  • 식별자관계 : 부모로부터 받은 식별자를 자식엔터티의 주식별자로 이용하는 경우는 Null값이 오면 안되므로 반드시 부모엔터티가 생성되어야 자기 자신의 엔터티가 생성되는 관계

    • 문제점 : PK수 증가
  • 비식별자관계 : 부모엔터티로부터 속성을 받았지만 자식엔터티의 주식별자로 사용하지 않고 일반적인 속성으로만 사용하는 관계

    • 문제점 : 부모엔터티의 PK조건으로 자식엔터티의 속성을 조회해야 할 때, 불필요한 join이 들어감

  • 제1정규형 : 모든 속성은 반드시 하나의 값을 가져야 한다. (Ex. 연락처 = 02-123-4567, 010-1234-5678 => x)(ex. Entity : 주문번호/상품번호1/상품명1/상품번호2/상품명2/고객번호/고객명 => x)

    • 부모-자식 엔터티 관계형성으로 해결, 자식엔터티로 생성
  • 제2정규형 : 엔터티의 일반속성은 주식별자 전체에 종속적이어야 한다. (Ex. 상품명은 오직 주식별자인 상품번호에 의해서만 결정된다, 주식별자가 상품번호 1개임을 가정)

    • M:M 관계형성으로 해결
  • 제3정규형 : 엔터티 일반속성 간에는 서고 종속적이지 않는다. (Ex. 고객번호는 주문번호에 종속, 고객명은 고객번호에 종속=>고객명은 주문번호에 종속 ==> 이행 종속성이므로 제3정규형 위반)

    • 부모-자식 엔터티 관계형성으로 해결, 위 예제에서는 고객을 부모엔터티로 생성

  • 반정규화 : 성능을 위해 데이터 중복을 허용, 정규화의 반대

    • 성능이 향상될 수 있는 경우 : 잦은 조회 쿼리의 join 횟수가 많은 경우
    • 성능이 저하될 수 있는 경우 : 불필요한 UPDATE로직이 추가될 수 있음

  • 계층형 데이터 모델 : 엔터티의 인스턴스간 계층이 존재할 때의 데이터 모델

  1. 데이터 조회 시, 셀프조인 발생

  • Null 속성의 이해
  1. Null 값의 연산은 언제나 Null
  2. 집계함수는 Null 값을 제외하고 처리한다.

  • SQL의 종류
  1. 데이터 조작어 (DML) : SELECT, INSERT, UPDATE, DELETE
  2. 데이터 정의어 (DDL) : CREATE, ALTER, DROP, RENAME
  3. 데이터 제어어 (DCL) : GRANT, REVOKE
  4. 트랜잭션 제어어 (TCL) : COMMIT, ROLLBACK

  • 합성연산자 : || 또는 CONCAT(string1, string2)

  • 단일행 함수의 종류

  1. 문자형 함수 : LOWER, UPPER, ASCII, CHR/CHAR, CONCAT, SUBSTR/SUBSTRING, LENGTH/LEN, LTRIM, RTRIM, TRIM
  2. 숫자형 함수 : ABS, SIGN, MOD, CEIL/CEILING, FLOOR, ROUND, TRUNC, SIN, COS, TAN, EXP, POWER, SQRT, LOG, LN
  3. 날짜형 함수 : SYSDATE/GETDATE, EXTRACT/DATEPART, TO_NUMBER(TO_CHAR(d, ‘YYYY’|’MM’’|DD’))/YEAR|MONTH|DAY
  4. 변환형 함수 : (CAST, TO_NUMBER, TO_CHAR, TO_DATE)/(CAST, CONVERT)
  5. NULL 관련 함수 : NVL/ISNULL, NULLIF, COALESCE

  • ASCII <-> CHR/CHAR

  • LTRIM : 첫 문자부터 확인해서 지정 문자가 나타나면 해당 문자를 제거 (디폴트는 공백)

  • TRIM : 문자열에서 머리말, 꼬리말 또는 양쪽에 있는 지정 문자를 제거 (디폴트는 both)

  • MOD : 숫자를 나누어 나머지 값을 리턴 (%로 대체 가능)

  • SIGN : 숫자가 양수인지, 음수인지 0인지를 구별

  • TRUNC : 숫자를 소수 m자리에서 잘라서 버림 (디폴트는 0)

  • POWER : 숫자의 거듭제곱 값을 리턴

  • EXTRACT/DATEPART : 날짜 데이터에서 연월일 데이터를 출력

  • CASE 표현

  1. CASE (표현식) WHEN 기준값1 THEN 값1 WHEN 기준값2 THEN 값2 ELSE 디폴트값 END
  2. Oracle 한정 : DECODE(표현식, [기준값1, 값1, 기준값2, 값2, … , 디폴트값]) : 표현식이 기준값1이면 값1을, 기준값2이면 값2를 리턴하고, 부합하는 기준값이 없을 경우, 디폴트값 리턴

  • NULLIF(식1, 식2) : 식1이 식2의 결과와 같을 경우 null, 다를 경우 식1 리턴

  • 연산자의 우선순위

  1. 괄호
  2. 비교 연산자, SQL 연산자
  3. NOT 연산자
  4. AND
  5. OR

  • 집계함수의 종류
  1. COUNT(*) : NULL 값을 포함한 행의 수를 출력
  2. COUNT(표현식) : 표현식의 값이 NULL 값인 것을 제외한 행 수를 출력
  3. STDDEV : 표준 편차를 출력
  4. VARIANCE/VAR : 분산을 출력

  • 집계함수는 WHERE 절에 올 수 없다.

  • GROUP BY는 NULL을 무시한다.

  • GROUP BY 보다 WHERE 절이 먼저 수행된다.

  • HAVING 절은 GROUP BY 절의 기준 항목이나 소그룹의 집계함수를 이용한 조건을 표시할 수 있다.

  • SELECT 문장 실행 순서

  1. FROM
  2. WHERE
  3. GROUP BY
  4. HAVING
  5. SELECT
  6. ORDER BY

  • EQUI JOIN : 두 테이블 간에 칼럼 값들이 서로 정확하게 일치하는 경우에 사용되는 방법

  • Non EQUI JOIN : 두 개의 테이블 간에 논리적인 연관 관계는 갖고 있으나, 칼럼 값들이 서로 일치하지 않는 경우에 사용 (등호가 아닌 부등호나 BETWEEN 사용)

  • OUTER JOIN : 조인 조건이 안맞아도 데이터를 조회하려고 할 때 사용되는 방법 (LEFT, RIGHT가 있음, 값이 없을 경우 NULL)

  • FROM 절의 JOIN 형태

  1. INNER JOIN : 조인 조건을 만족하는 행들만 반환
  2. NATURAL JOIN : 두 테이블 간에 동일한 이름을 갖는 모든 칼럼들에 대해 EQUI JOIN 수행
  3. USING 조건절 : NATURAL JOIN에서 USING 조건절을 활용하여 원하는 칼럼에 대해서만 선택적으로 EQUI JOIN 수행
  4. ON 조건절 : JOIN 조건 설정, 칼럼명이 달라도 JOIN 가능
  5. CROSS JOIN : 두 테이블간 JOIN 조건이 없는 경우 생길 수 있는 모든 데이터의 조합 (M * N 건의 데이터 조합 발생)
  6. OUTER JOIN

  • 동작하는 방식에 따른 서브 쿼리 분류
  1. 비연관 서브 쿼리 : 서브 쿼리가 메인 쿼리 컬럼을 갖고 있지 않는 형태의 서브 쿼리다. 메인 쿼리에 값을 제공하기 위한 목적으로 주로 사용한다.
  2. 연관 서브 쿼리 : 서브 쿼리가 메인 쿼리 칼럼을 갖고 있는 형태의 서브 쿼리다. 일반적으로 메인 쿼리가 먼저 수행돼 읽혀진 데이터를 서브 쿼리에서 조건이 맞는지 확인하고자 할 때 주로 사용한다. 서브 쿼리 내에 메인 쿼리 칼럼이 사용된 서브 쿼리

  • 반환되는 데이터의 형태에 따른 서브 쿼리 분류
  1. Single Row 서브 쿼리 : 서브 쿼리의 실행 결과가 항상 1건 이하인 서브 쿼리, 단일 행 비교 연산자와 함께 사용 (ex. =, < 등)
  2. Multi Row 서브 쿼리 : 서브 쿼리의 실행 결과가 여러 건인 서브 쿼리, 다중 행 비교 연산자와 함께 사용 (ex. In, all, exists 등)
  3. Multi Column 서브 쿼리 : 서브 쿼리의 실행 결과로 여러 칼럼을 반환

  • 다중 행 비교 연산자
  1. IN : 서브 쿼리의 결과에 존재하는 임의의 값과 동일한 조건
  2. ALL : 서브 쿼리의 결과에 존재하는 모든 값을 만족하는 조건
  3. ANY : 서브 쿼리의 결과에 존재하는 어느 하나의 값이라도 만족하는 조건
  4. EXISTS : 서브 쿼리의 결과가 존재하는지 여부를 확인하는 조건

  • 뷰 사용의 장점
  1. 독립성 : 테이블 구조가 변경돼도 뷰를 사용하는 응용 프로그램은 변경하지 않아도 된다.
  2. 편리성 : 복잡한 질의를 뷰로 생성함으로써 관련 질의를 단순하게 작성할 수 있다.
  3. 보안성 : 숨기고 싶은 정보는 빼고 생성하여 사용자에게 정보를 감출 수 있다.

  • 집합연산자
  1. UNION : 개별 SQL 문의 결과에 대해 합집합 연산을 수행
  2. UNION ALL : 개별 SQL 문의 결과에 대해 합집합 연산을 수행하며, 중복된 행도 그대로 표시
  3. INTERSECT : 개별 SQL 문의 결과에 대해 교집합 연산을 수행
  4. EXCEPT : 개별 SQL 문의 결과에 대해 차집합 연산을 수행

  • ROLLUP : GROUP BY 칼럼의 GROUP 별 집계 (TOTAL) 수행 및 GROUP 정렬, 상세 칼럼도 정렬이 필요할 경우, ORDER BY 도 병행 사용

  • GROUPING : 소계가 계산된 결과에는 1이 표시됨, 그렇지 않은 경우는 0이 표시됨

  • GROUPING SETS : GROUP BY 모든 칼럼에 대해 GROUPING 수행, 칼럼 순서가 바껴도 조회결과는 같음 (ex. GROUP BY GROUPING SETS (A, B) : count(A) + count(B) 개의 칼럼 조회)

  • WINDOW FUNCTION SYNTAX : SELECT WINDOW_FUNCTION (ARGUMENTS) OVER ([PARTITION BY 칼럼] [ORDER BY 절] [WINDOWING 절]) FROM 테이블명;

  1. ARGUMENTS : 함수에 따라 0 ~ N개의 인수가 지정될 수 있다.
  2. PARTITION BY 절 : 전체 집합을 기준에 의해 소그룹으로 나눌 수 있다.
  3. ORDER BY 절 : 어떤 항목에 대해 순위를 지정할지 ORDER BY 절을 기술한다.
  4. WINDOWING 절 : WINDOWING 절은 함수의 대상이 되는 행 기준의 범위를 강력하게 지정할 수 있다.

  • RANK : ORDER BY를 포함한 QUERY 문에서 특정 항목에 대한 순위를 구하는 함수, PARTITION 포함 시, 특정 컬럼별로 RANK가 지정됨

  • DENSE_RANK : RANK 함수와 유사하나 동일한 순위를 하나의 건수로 취급

  • ROW_NUMBER : RANK나 DENSE_RANK 함수와는 다르게 동일한 값이라도 고유한 순위를 부여

  • FIRST_VALUE : 파티션별 윈도우에서 가장 먼저 나온 값 (SQL_SERVER (x))

  • LAST_VALUE : 파티션별 윈도우에서 가장 나중에 나온 값 (SQL_SERVER (x))

  • LAG : 파티션별 윈도우에서 이전 몇 번째 행의 값 (SQL_SERVER (x))

  • LEAD : 파티션별 윈도우에서 이후 몇 번째 행의 값 (SQL_SERVER (x))

  • RATIO_TO_REPORT : 파티션 내 전체 SUM(칼럼) 값에 대한 행별 칼럼 값의 백분율을 소수점으로 구할 수 있음 (SQL_SERVER (x))

  • PERCENT_RANK : 파티션별 윈도우에서 제일 먼저 나오는 것을 0으로, 제일 늦게 나오는 것을 1로 해, 값이 아닌 행의 순서별 백분율을 구한다. (SQL_SERVER (x))

  • CUME_DIST : 파티션별 윈도우의 전체 건수에서 현재 행보다 작거나 같은 건수에 대한 누적백분율을 구한다. 결과 값은 > 0 & <= 1 (SQL_SERVER (x))

  • NTILE : 파티션별 전체 건수를 ARGUMENT 값으로 N 등분한 결과, N개씩 그룹이 나누어짐

  • TOP (Expression) [PERCENT] [WITH TIES] : SQL Server 한정

  1. Expression : 반환할 행 수를 지정하는 숫자
  2. PERCENT : 쿼리 결과 집합에서 처음 Expression%의 행만 반환
  3. WITH TIES : ORDER BY 절이 지정된 경우에만 사용, TOP N(PERCENT)의 마지막 행과 같은 값이 있는 경우 추가 행이 출력되도록 지정 가능

  • ROW LIMITTING절 (ORDER BY 절 다음에 기술)
  1. Syntax1 : [OFFSET offset {ROW | ROWS}]
  2. Syntax2 : [FETCH {FIRST | NEXT} [{row count | percent PERCENT}] {ROW | ROWS} {ONLY | WITH TIES}]
  3. OFFSET offset : 건너뛸 행의 개수를 지정
  4. FETCH : 반환할 행의 개수나 백분율을 지정
  5. ONLY : 지정된 행의 개수나 백분율만큼 행을 반환
  6. WITH TIES : 마지막 행에 대한 동순위를 포함해서 반환

  • Oracle 계층형 질의
    SELECT …
    FROM 테이블
    WHERE condition
    AND condition
    START WITH condition
    AND condition
    CONNECT BY [NOCYCLE] condition
    AND condition
    [ORDER SIBLINGS BY column, column, …]
  1. START WITH : 계층 구조 전개의 시작 위치를 지정하는 구문
  2. CONNECT BY : 다음에 전개될 자식 데이터를 지정하는 구문
  3. PRIOR : CONNECT BY절에 사용되며, 현재 읽은 칼럼을 지정, (FK) = PRIOR (PK) 형태를 사용하면 부모 데이터에서 자식 데이터 방향으로 전개하는 순방향 전개, (PK) = PRIOR (FK) 형태를 사용하면 반대로 자식 데이터에서 부모 데이터 방향으로 전개하는 역방향 전개
  4. NOCYCLE : 데이터를 전개하면서 이미 나타났던 동일한 데이터가 전개 중에 다시 나타나는 사이클이 발생 시, 런타임 오류가 발생. NOCYCLE을 추가하면 오류를 발생시키지 않고 사이클이 발생한 이후의 데이터를 전개하지 않는다.
  5. ORDER SIBLINGS BY : 형제 노드 사이에서 정렬을 수행
  6. WHERE : 모든 전개를 수행한 후에 지정된 조건을 만족하는 데이터만 추출

  • 계층형 질의에서 사용되는 가상 칼럼
  1. LEVEL : 루트 데이터이면 1, 그 하위 데이터이면 2다. 리프(Leaf) 데이터까지 1씩 증가한다.
  2. CONNECT_BY_ISLEAF : 전개 과정에서 해당 데이터가 리프 데이터이면 1, 그렇지 않으면 0이다.
  3. CONNECT_BY_ISCYCLE : 전개 과정에서 자식을 갖는데, 해당 데이터가 조상으로서 존재하면 1, 그렇지 않으면 0

  • 계층형 질의에서 사용되는 함수
  1. SYS_CONNECT_BY_PATH : 루트 데이터부터 현재 전개할 데이터까지의 경로를 표시
  2. CONNECT_BY_ROOT : 현재 전개할 데이터의 루트 데이터를 표시

  • PIVOT절 : 행을 열로 회전

  • UNPIVOT절 : 열을 행으로 회전

  • 정규표현식 POSIX 연산자

  1. . : 모든 문자와 일치 (newline 제외)
  2. | : 대체 문자를 구분
  3. \ : 다음 문자를 일반 문자로 취급
  4. ^ : 문자열의 시작
  5. $ : 문자열의 끝
  6. ? : 0회 또는 1회 일치 (greedy : 패턴 최소 일치)
  7. ?? : 0회 또는 1회 일치 (nongreedy : 패턴 최대 일치)
  8. * : 0회 또는 그 이상의 횟수로 일치 (greedy : 패턴 최소 일치)
  9. *? : 0회 또는 그 이상의 횟수로 일치 (nongreedy : 패턴 최대 일치)
  10. + : 1회 또는 그 이상의 횟수로 일치 (greedy : 패턴 최소 일치)
  11. +? : 1회 또는 그 이상의 횟수로 일치 (nongreedy : 패턴 최대 일치)
  12. {m} : m회 일치 (greedy : 패턴 최소 일치)
  13. {m}? : m회 일치 (nongreedy : 패턴 최대 일치)
  14. {m,} : 최소 m회 일치 (greedy : 패턴 최소 일치)
  15. {m,}? : 최소 m회 일치 (nongreedy : 패턴 최대 일치)
  16. {,m} : 최대 m회 일치 (greedy : 패턴 최소 일치)
  17. {,m}? : 최대 m회 일치 (nongreedy : 패턴 최대 일치)
  18. {m,n} : 최소 m회, 최대 n회 일치 (greedy : 패턴 최소 일치)
  19. {m,n}? : 최소 m회, 최대 n회 일치 (nongreedy : 패턴 최대 일치)
  20. (expr) : 괄호 안의 표현식을 하나의 단위로 취급
  21. [char…] : 문자 리스트 중 한 문자와 일치
  22. [^char…] : 문자 리스트에 포함되지 않은 한 문자와 일치
  23. [-] : [0-9] [a-z] [A-Z] [a-zA-Z] [0-9a-zA-Z] [0-9a-fA-F]
  24. \d : 숫자
  25. \D : 숫자가 아닌 모든 문자
  26. \w : 숫자와 영문자(underbar 포함)
  27. \W : 숫자와 영문자가 아닌 모든 문자(underbar 제외)
  28. \s : 공백 문자
  29. \S : 공백 문자가 아닌 모든 문자

  • REGEXP_SUBSTR : 문자열에서 일치하는 패턴을 반환

  • REGEXP_LIKE : 문자열이 패턴과 일치하면 TRUE, 아니면 FALSE 반환

  • REGEXP_REPLACE : 일치하는 패턴을 replace_string으로 변경한 문자로 반환

  • REGEXP_SUBSTR : 일치하는 패턴의 문자만을 반환

  • REGEXP_INSTR : 일치하는 패턴의 시작 위치를 정수로 반환

  • REGEXP_COUNT : 일치하는 패턴의 횟수를 반환

  • MERGE : 새로운 행을 입력하거나, 기존 행을 수정하는 작업을 한번에 할 수 있음

  • 트랜잭션의 특성

  1. 원자성 : 트랜잭션에서 저으이된 연산들은 모두 성공적으로 실행되던지 아니면 전혀 실행되지 않은 상태로 남아 있어야 한다.
  2. 일관성 : 트랜잭션이 실행되기 전의 데이터베이스 내용이 잘못 돼 있지 않다면 트랜잭션이 실행된 이후에도 데이터베이스의 내용에 잘못이 있으면 안된다.
  3. 고립성 : 트랜잭션이 실행되는 도중에 다른 트랜잭션의 영향을 받아 잘못된 결과를 만들어서는 안된다.
  4. 지속성 : 트랜잭션이 성공적으로 수행되면, 그 트랜잭션이 갱신한 데이터베이스의 내용은 영구적으로 저장된다.

  • COMMIT or ROLLBACK 이전 상태
  1. 이전 상태로 복구 가능
  2. 현재 사용자는 SELECT 문장으로 결과를 확인할 수 있다.
  3. 다른 사용자는 현재 사용자가 수행한 명령의 결과를 볼 수 없다.
  4. 변경된 행은 잠금(LOCKING)이 설정돼서 다른 사용자가 변경할 수 없다.

  • SQL Server에서의 트랜잭션
  1. AUTO COMMIT : SQL Server의 기본 방식, DBMS가 트랜잭션을 컨트롤하는 방식, 명령어가 성공적으로 수행되면 자동으로 COMMIT 수행, 오류가 발생하면 자동으로 ROLLBACK 수행
  2. 암시적 트랜잭션 : Oracle과 같은 방식, 트랜잭션의 시작은 DBMS가 처리, 트랜잭션의 끝은 사용자가 명시적으로 COMMIT 또는 ROLLBACK 처리
  3. 명시적 트랜잭션 : 트랜잭션의 시작과 끝을 모두 사용자가 명시적으로 지정하는 방식

  • SAVEPOINT : 저장점(SAVEPOINT)를 지정하면 ROLLBACK할 때 트랜잭션에 포함된 전체 작업을 롤백하는 것이 아니라, 현 시점에서 SAVEPOINT까지 트랜잭션의 일부만 롤백할 수 있다.

  • 제약조건의 종류

  1. PRIMARY KEY : 테이블에 저장된 행 데이터를 고유하게 식별하기 위한 기본키를 정의한다.
  2. UNIQUE : 테이블에 저장된 행 데이터를 고유하기 식별하기 위한 고유키를 정의한다.
  3. NOT NULL : NULL값의 입력을 금지한다.
  4. CHECK : 입력할 수 있는 값의 범위 등을 제한한다.
  5. FOREIGN KEY : 관계형 데이터베이스에서 테이블 간의 관계를 정의하기 위해 기본키를 다른 테이블의 외래키로 복사하는 경우 외래키가 생성된다.

  • Oracle에서 제공하는 유저들
  1. SCOTT : Oracle 테스트용 샘플 계정
  2. SYS : 백업 및 복구 등 데이터베이스 상의 모든 관리 기능을 수행할 수 있는 최상위 관리자 계정
  3. SYSTEM : 백업, 복구 등 일부 관리 기능을 제외한 모든 시스템 권한을 부여받은 DBA 계정

반응형

'개발 > DB' 카테고리의 다른 글

쿼리튜닝기 (2)  (1) 2024.01.23
쿼리튜닝기 (1)  (1) 2023.07.27
728x90
반응형

Spring

File to DB 개발의 마무리 단계인 Spring Scheduler 개발과정에 대해 정리할 것이다.

이전 게시글까지 File to DB 배치개발은 모두 완료되었다.
대용량의 File 데이터를 읽어서 DB저장하는 부분까지는 우여곡절 끝에 구현하였다. 👉 이전글 참고

아직 남은 요구사항

그리고 아직 남은 요구사항이 있었다.

  1. File to DB 배치의 실행주기는 관리자가 임의로 조정 가능할 것.
  2. 관리자의 조정은 런타임 환경에서도 가능해야 함.
  3. 배치 기능은 실행 서버를 선택할 수 있어야 함.

현재 아래와 같은 @Scheduled 어노테이션 설정 방식으로는 런타임 환경에서 관리자가 임의로 스케줄러 설정을 변경하지 못할 뿐만 아니라,
스케줄러 실행환경도 임의로 선택하지 못한다.

이러한 점을 개선하기 위해 Scheduler 기능의 추가적인 개발이 필요했다.

  • JobScheduler.java
@Component
@RequiredArgsConstructor
public class JobScheduler {

    private final JobLauncher jobLauncher;

    private final InterfaceFileReaderBatchJobConfig interfaceFileReaderBatchJobConfig;

    private final InterfaceReader interfaceReader;

    // 스케줄러 주기 설정, 어노테이션 선언 방식
    @Scheduled(cron = "0 * * * * *")
    public void runJob() throws IOException {

        ...

    }

}



Scheduler 동작원리

Scheduler 기능 개발에 앞서서 동작원리를 간단하게 살펴본다.




위 사진처럼 Scheduler를 통해 실행될 Task는 TaskScheduler에 주입되어 사용되고, Scheduler의 실행정보는 Trigger에 주입되어 사용된다.
그리고 Scheduler 기능은 위 두가지 추상클래스를 이용하여 실행되는 구조이다.

Scheduler 초기화 순서

  1. Scheduler가 실행될 root 클래스에 선언된 @EnableScheduling 어노테이션을 통해 Scheduler 초기화 작업이 진행된다.
  2. @EnableScheduling 어노테이션에 import된 SchedulingConfiguration 클래스가 ScheduledAnnotationBeanPostProcessor를 Bean으로 등록 ( ScheduledAnnotationBeanPostProcessor을 통해 스케줄링에 필요한 실질적인 초기화 작업이 수행됨 )
  3. ScheduledAnnotationBeanPostProcessor 클래스가 @Scheduled 어노테이션이 붙은 메소드를 스케줄러로 등록
  4. @Scheduled 어노테이션에 설정된 값(ex. fixedDelay, cron 등)에 맞게 task를 생성하여 ScheduledTaskRegistrar에 등록, 실제 작업은 Runnable에 할당 ( cron 표기법이 사용되었을 경우, CronTrigger가 등록됨 )

위 과정을 통해 ScheduledTaskRegistrar에 task가 등록되면, TaskScheduler가 등록된 task를 일정 주기에 맞게 실행시켜 준다.

TaskScheduler 동작 순서

  1. TaskScheduler 클래스의 schedule 메소드가 파라미터로 task정보를 받아 실행된다.
  2. 일정주기 이후에 실행될 작업을 생성하기 위해 파라미터로 받은 task 정보를 기반으로 ReschedulingRunnable 을 생성한다.
  3. 일정주기 이후에 run 메소드가 실행되어 작업이 수행되면, 다시 schedule 메소드를 호출하여 ReschedulingRunnable 을 생성한다.
    (반복)

위 과정이 반복되면서 등록된 task가 일정주기마다 실행된다.

Scheduler 기능 개발 : ThreadPoolTaskScheduler 활용

지금까지 Scheduler의 동작원리를 살펴보았다.
이제 다시 돌아와서, 요구사항을 충족시키기 위해 Scheduler의 추가적인 개발을 진행해보자.

요구사항에 따르면,
Scheduler는 어느 서버에서 실행되어야 하는지, 얼만큼의 주기를 가져야 하는지 런타임 환경에서 조정할 수 있어야 한다.
이를 위해 ThreadPoolTaskScheduler 객체를 활용할 수 있다.

ThreadPoolTaskScheduler란?

ThreadPoolTaskSchedulerConcurrentTaskScheduler 와 같이 TaskScheduler에 등록될 수 있는 Bean이다.
둘 중 ConcurrentTaskScheduler@EnableScheduling 어노테이션을 통해 default로 TaskScheduler에 등록되어,
위에서 언급한 Scheduler 초기화 작업을 수행한다.

다른 하나인 ThreadPoolTaskScheduler 는 사용자가 해당 Bean을 생성하면, @EnableScheduling 어노테이션을 통해 추가적으로 TaskSchedulerThreadPoolTaskScheduler Bean도 등록해준다.

그리고 우리는 ThreadPoolTaskScheduler 를 통해 커스터마이징한 Scheduler를 등록할 수 있다.

위와 같이 Scheduler를 커스터마이징할 수 있는 ThreadPoolTaskScheduler 의 강력한 특징을 활용하여 런타임환경에서도 Scheduler의 설정을 변경할 수 있도록 추가적인 기능 개발을 진행할 것이다.

Scheduler 기능 개발 수행

기능 개발을 위해 아래와 같이 코드를 구성하였다.

  • ThreadPoolTaskSchedulerConfig.java
@Configuaration
public class ThreadPoolTaskSchedulerConfig {

    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(1);                        // Scheduler task 실행을 위한 thread의 갯수
        taskScheduler.setThreadNamePrefix("jobScheduler");    // thread 이름
        taskScheduler.initialize();                            // 초기화
        return taskScheduler;
    }

}

위 설정 코드를 통해 Scheduler task 수행을 위한 thread를 몇 개를 생성할지 결정하였다.
아직 Scheduler 기능은 소규모이고, 배치 job들이 비동기로 수행되어 발생되는 예기치못한 오류들을 방지하기 위해
일단 1개만 생성하기로 하였다.

Scheduler task 수행에 멀티스레딩 방식이 필요할 시,
해당 thread의 갯수를 늘리고, 배치 job간 공유하여 사용하고 있는 자원들을 파악하여 멀티스레딩 환경에 맞게 추가개발이 필요할 것이다.

  • SchedulerConfig.java
@Table(name = "TB_SCHEDULER_CONFIG")
@Data
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity
public class SchedulerConfig {

    @Id
    @Column(name = "scheduler_idx", length = 2, nullable = false)
    private String schedulerIdx;

    @Column(name = "scheduler_name", length = 50, nullable = false)
    private String schedulerName;

    @Column(name = "cron", length = 50, nullable = false)
    private String cron;

    @Column(name = "execution_yn", length = 1, nullable = false)
    private String executionYn;

    @Column(name = "execution_ip", length = 20, nullable = false)
    private String executionIp;

    @Column(name = "last_chg_dt")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date lastChgDt;

}
  • SchedulerConfigRepository.java
public interface SchedulerConfigRepository extends JpaRepository<SchedulerConfig, Long> {

    SchedulerConfig findBySchedulerIdxAndSchedulerName(String scheduler_idx, String scheduler_name);

}

위와 같이 각 Scheduler의 설정정보를 담기 위한 테이블 및 Entity를 생성하였다.
각 필드에 대한 desc은 다음과 같다.

  1. scheduler_idx : Scheduler를 식별하기 위한 index
  2. scheduler_name : Scheduler의 이름 (index와 매핑)
  3. cron : Scheduler 실행 주기를 결정할 cron식
  4. execution_yn : AP 내부에서의 Scheduler 실행여부를 결정
  5. execution_ip : Scheduler가 실행될 서버의 ip
  6. last_chg_dt : 설정 최종변경 일시

런타임환경 실행주기 변경, 특정 서버에서만 실행, 실행 여부 변경이라는 요구사항을 충족시키기 위해
위와 같이 설정테이블을 구성한 것이다.

그리고 각 Scheduler가 위 테이블에 저장된 설정 값을 기반으로 실행될 수 있도록 Scheduler 코드를 작성할 것이다.

  • Sftp1Scheduler.java
@Slf4j
@Component
@RequiredArgsConstructor
public class Sftp1Scheduler {

    // Spring Batch Bean
    private final JobLauncher jobLauncher;
    private final ThreadPoolTaskScheduler threadPoolTaskScheduler;
    private final InterfaceFileReaderBatchJobConfig interfaceFileReaderBatchJobConfig;
    private final Sftp1Reader sftp1Reader;

    // Scheduler 설정 테이블 Jpa Repository
    private final SchedulerConfigRepository schedulerConfigRepository;

    // Scheduler 식별자
    private String schedulerIdx = "3";
    private String schedulerName = "Sftp1";

    // Scheduler 설정 값
    private String executionIp;
    private String executionYn;
    private String cron;

    // Scheduler가 실행시킬 task
    private ScheduledFuture<?> future;

    // 초기화
    @PostConstruct // AP가 실행되고 Spring 초기화 과정에서 메소드가 실행될 수 있도록 선언
    public void init() {
        try {
            if (!this.startScheduler()) {
                log.error();
            }
        }
        catch (Exception e) {
               log.error();
        }
    }

    // 스케줄러 시작
    public boolean startScheduler() throws Exception {
        if (this.future != null)
            return false;
        SchedulerConfig schedulerConfig = schedulerConfigRepository.findBySchedulerIdxAndSchedulerName(this.schedulerIdx, this.schedulerName);
        if (!validateExecution(schedulerConfig)) {
            return false;
        }
        this.executionIp = schedulerConfig.getExecutionIp();
        this.executionYn = schedulerConfig.getExecutionYn();

        this.cron = schedulerConfig.getCron();
        ScheduledFuture<?> future = this.threadPoolTaskScheduler.schedule(this.getRunnable(), this.getTrigger());
        this.future = future;
        return true;
    }

    // 스케줄러 종료
    public void stopScheduler() {
        if (this.future != null)
            this.future.cancel(true);
        this.future = null;
    }

    // 런타임 스케줄러 주기 변경
    public void changeCron(String cron) throws Exception {
        this.saveCron(cron);
        this.stopScheduler();
        this.cron = cron;
        this.startScheduler();
    }

    // 여기부터 클래스 내부에서만 쓸 메소드들
    private boolean validateExecution(SchedulerConfig schedulerConfig) throws UnknowsHostException {
        if (schedulerConfig == null) {
            return false;
        }
        String localIp = InetAddress.getLocalHost().getHostAddress();    // 현재 실행중인 서버 ip 조회

        if ("N".equals(schedulerConfig.getExecutionYn()) || !localIp.equals(schedulerConfig.getExecutionIp())) {
            return false;
        }
        return true;
    }

    // 스케줄러가 실행시킬 task
    private Runnable getRunnable() {
        return () -> {
            try {

                Step interfaceStep = interfaceFileReaderBatchJobConfig.interfaceStep(sftp1Reader.resultFlatFileItemReader);

                Map<String, JobParameter> confMap = new HashMap<>();
                confMap.put("time", new JobParameter(System.currentTimeMillis()));
                JobParameters jobParameters = new JobParameters(confMap);

                jobLauncher.run(interfaceFileReaderBatchJobConfig.interfaceJob(interfaceStep), jobParameters);

            }
            catch (JobExecutionAlreadyRunningException | JobInstanceAlreadyCompleteException | JobParametersInvalidException | JobRestartException e) {
                log.error();
            }
            catch (Exception e) {
                log.error();
            }
        }
    }

    // 스케줄러의 trigger
    private Trigger getTrigger() {
        return new CronTrigger(this.cron);
    }

    @Transactional
    private void saveCron(String cron) throws Exception {
        SchedulerConfig schedulerConfig = new SchedulerConfig();
        schedulerConfig.setSchedulerIdx(this.schedulerIdx);
        schedulerConfig.setSchedulerName(this.schedulerName);
        schedulerConfig.setCron(cron);
        schedulerConfig.setLastChgDt(new Date());
        schedulerConfig.setExecutionIp(this.executionIp);
        schedulerConfig.setExecutionYn(this.executionYn);
        schedulerConfigRepository.save(schedulerConfig);
    }

}

위와 같이 Scheduler 코드를 작성하여 런타임 환경에서 사용자 임의대로 스케줄러 설정을 변경할 수 있도록 하였다.
초기화 flow는 다음과 같다.

초기화 flow

  • AP 실행 및 Spring 초기화
    • init 메소드 실행
      • startScheduler 메소드 실행
        • 스케줄러 설정 테이블 값 조회
        • 조회 값 기반 validateExecution 메소드 실행
          • 서버ip, 실행여부 확인 및 결정
        • threadPoolTaskScheduler.schedule 실행
          • 다음 주기에 실행될 task 등록


일단 위 코드를 통해 임의 서버에서 실행여부에 맞게 Scheduler가 실행되도록 startScheduler, validateExecution 메소드를 작성하였다.
그리고 changeCron 및 stopScheduler 메소드를 작성하여 런타임환경에서 사용자의 요청에 따라 Scheduler를 제어할 수 있도록 준비하였다.

이제 실제로 사용자의 요청을 받기 위한 Controller 구현이 필요했다.

  • RequestSchedulerDto.java
@Data
public class RequestSchedulerDto {

    @JsonProperty("schedulerIdx")
    private String schedulerIdx;

    @JsonProperty("schedulerName")
    private String schedulerName;

    @JsonProperty("cron")
    private String cron;

}
  • ResponseSchedulerDto.java
@Data
public class ResponseSchedulerDto {

    @JsonProperty("rsltcd")
    private String rsltCd;

    @JsonProperty("errMsg")
    private String errMsg;

}
  • SchedulerController.java
@Controller
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/scheduler")
public class SchedulerController {

    // 스케줄러 Bean 주입
    private final Sftp1Scheduler sftp1Scheduler;
    private final Sftp2Scheduler sftp2Scheduler;

    // sheduler 작업 시작
    @PostMapping("/start")
    @ResponseBody
    public ResponseEntity<?> requestStartScheduler(@RequestBody RequestSchedulerDto requestSchedulerDto) throws InterruptedException {
        ResponseSchedulerDto responseSchedulerDto = new ResponseSchedulerDto();
        String schedulerIdx = responseSchedulerDto.getSchedulerIdx();
        String schedulerName = responseSchedulerDto.getSchedulerName();
        boolean returnFlag = true;

        if (schedulerIdx == null || schedulerName == null) {
            responseSchedulerDto.setRsltCd("E1");
            responseSchedulerDto.setErrMsg("no request data");
        }

        try {
            if ("1".equals(schedulerIdx) && "InterfaceResult".equals(schedulerName)) {
                returnFlag = sftp1Scheduler.startScheduler();
            }
            else if ("2".equals(schedulerIdx) && "InterfaceResult2".equals(schedulerName)) {
                returnFlag = sftp2Scheduler.startScheduler();
            }
            else {
                log.error("no target scheduler");
                responseSchedulerDto.setRsltCd("E4");
                responseSchedulerDto.setErrMsg("no target scheduler");
                return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
            }

            if (returnFlag == false) {
                responseSchedulerDto.setRsltCd("E3");
                responseSchedulerDto.setErrMsg("already started scheduler or execution conditions not met");
            }
            responseSchedulerDto.setRsltCd("S");
            responseSchedulerDto.setErrMsg("");
        }
        catch (Exception e) {
            log.error(e.getMessage());
            responseSchedulerDto.setRsltCd("E2");
            responseSchedulerDto.setErrMsg("internal server error");
        }
        return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
    }

    // scheduler 작업 종료
    @PostMapping("/stop")
    @ResponseBody
    public ResponseEntity<?> requestStopScheduler(@RequestBody RequestSchedulerDto requestSchedulerDto) throws InterruptedException {
        ResponseSchedulerDto responseSchedulerDto = new ResponseSchedulerDto();
        String schedulerIdx = responseSchedulerDto.getSchedulerIdx();
        String schedulerName = responseSchedulerDto.getSchedulerName();

        if (schedulerIdx == null || schedulerName == null) {
            responseSchedulerDto.setRsltCd("E1");
            responseSchedulerDto.setErrMsg("no request data");
        }

        try {
            if ("1".equals(schedulerIdx) && "InterfaceResult".equals(schedulerName)) {
                returnFlag = sftp1Scheduler.stopScheduler();
            }
            else if ("2".equals(schedulerIdx) && "InterfaceResult2".equals(schedulerName)) {
                returnFlag = sftp2Scheduler.stopScheduler();
            }
            else {
                log.error("no target scheduler");
                responseSchedulerDto.setRsltCd("E4");
                responseSchedulerDto.setErrMsg("no target scheduler");
                return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
            }

            responseSchedulerDto.setRsltCd("S");
            responseSchedulerDto.setErrMsg("");
        }
        catch (Exception e) {
            log.error(e.getMessage());
            responseSchedulerDto.setRsltCd("E2");
            responseSchedulerDto.setErrMsg("internal server error");
        }
        return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
    }

    // cron식 변경
    @PostMapping("/changecron")
    @ResponseBody
    public ResponseEntity<?> requestChangeTrigger(@RequestBody RequestSchedulerDto requestSchedulerDto) throws InterruptedException {
        ResponseSchedulerDto responseSchedulerDto = new ResponseSchedulerDto();
        String schedulerIdx = responseSchedulerDto.getSchedulerIdx();
        String schedulerName = responseSchedulerDto.getSchedulerName();
        String cron = responseSchedulerDto.getCron();

        if (schedulerIdx == null || schedulerName == null || cron == null) {
            responseSchedulerDto.setRsltCd("E1");
            responseSchedulerDto.setErrMsg("no request data");
        }

        try {
            if ("1".equals(schedulerIdx) && "InterfaceResult".equals(schedulerName)) {
                returnFlag = sftp1Scheduler.changeCron();
            }
            else if ("2".equals(schedulerIdx) && "InterfaceResult2".equals(schedulerName)) {
                returnFlag = sftp2Scheduler.changeCron();
            }
            else {
                log.error("no target scheduler");
                responseSchedulerDto.setRsltCd("E4");
                responseSchedulerDto.setErrMsg("no target scheduler");
                return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
            }

            responseSchedulerDto.setRsltCd("S");
            responseSchedulerDto.setErrMsg("");
        }
        catch (Exception e) {
            log.error(e.getMessage());
            responseSchedulerDto.setRsltCd("E2");
            responseSchedulerDto.setErrMsg("internal server error");
        }
        return new ResponseEntity<>(responseSchedulerDto, HttpStatus.OK);
    }

}

위와 같이 코드를 작성하여 실제 사용자가 scheduler 작업을 controll 할 수 있게 하였다.

Controller는 아래와 같이 3개로 구성하였다.

  • requestStartScheduler
  • requestStopScheduler
  • requestChangeTrigger


Controller의 구성은 셋 다 비슷한데, 기본적인 flow는 아래와 같다.

  1. Rest API body 값을 읽어서 사용자의 요청 값을 받는다.
  2. 받은 요청 값을 통해 어떤 Scheduler를 실행시킬지 결정한다. (if-else)
  3. Scheduler가 결정되면, 요청에 맞는 Scheduler의 메소드를 실행시킨다.

개선할 점.

  1. Scheduler 클래스 -> 인터페이스 및 부모-자식 클래스로 변경
    : 각 Scheduler 클래스의 구성은 비슷하다. Interface 및 부모-자식 클래스로 리팩토링 하여
    코드의 양을 현저하게 줄이고, 코드의 유지보수성을 높일 수 있다. 그리고 이를 통해 Controller의 if-else문을 개선할 수 있을 것 같다.
  2. Controller if-else
    : 현행은 if-else문을 통해 Scheduler를 식별하는 방식이다. 위 1번 항목에서 리팩토링된 코드를 활용하면
    scheduler가 추가될 때마다 else if를 추가하지 않아도 작동될 수 있도록 개발할 수 있지않을까 생각된다. => 검토 필요

위에서 언급한 개선 점은 👉다음글에서 어느정도 보완하였다.

마무리

본 글을 마지막으로 File to DB 개발기를 끝낸다.

이외에도 비대면 고객케어 솔루션 프로젝트를 통해 개발한 부분이 있다. (Spring Security, SOAP연동)
해당 부분은 좀 더 정리가 되면 이어서 작성할 것이다.

File to DB 개발을 수행했던 3월 한달 간 너무나 재밌고 유익한 개발을 경험했다.
Spring Boot의 기본 동작원리 뿐만 아니라, Scheduler, Batch 등과 같은 Spring 모듈에 대해서도 알게되어 좋았다.

또, 개발하며 느낀건... 내가 미지의 부분을 개발하며 하나씩 알아가는 것에 재미를 느낀다는 것을 알게 되었다.
개발하며 결과물을 내는 것도 좋지만, 앞으로는 개발을 진행하며 궁금증을 하나씩 해소해 나가는 과정에 초점을 두어야겠다.
그리고 이를 통해 꾸준하게 성장해 나갈 것이다.

반응형
728x90
반응형

Spring

현재까지 File을 읽어서 DB에 저장하는 기본적인 Batch 로직을 구성하였다. 👉이전글 참고

동일 File 반복 Read 에러 발생

현상

Batch 동작을 통해 SFTP 기능 테스트를 진행하는 도중에 한 가지 오류를 발견했다.
아래처럼 의도한대로 기능이 동작하지 않는 것이다.

  • 의도
  1. 주기적으로 Batch Job이 실행되어 Read 대상의 File을 선정한다.
  2. Read할 파일의 파일명 끝에 _END가 붙어있으면 skip하고 다음 대상을 탐색한다.
  3. Read 대상파일 선정 후, Read 작업이 끝나면 해당 파일의 파일명 끝에 _END를 붙여준다.

파일명 변경 로직은 아직 개발되지 않아서 수동으로 파일명 변경하고 테스트를 반복하였다.

프로그램이 대상파일을 Read하면 해당 파일에 수동으로 _END를 붙여주고,
다음 Batch Job 동작 시, 다른 대상파일을 탐색하는지 확인하는 테스트를 진행하였다.
하지만, 프로그램은 다른 파일을 탐색하지 않고 기존에 Read했던 파일만 반복적으로 Read하였다.

같은 파일만 지속적으로 Read하는 것을 봤을 때, 배치 job이 Read할 File을 새롭게 갱신을 못하는 것 같았다.
배치 job 실행 때마다 Step Bean이 새로 생성될 수 있도록 초점을 맞추고 해결방안을 고민하였다.

@StepScope

@StepScope는 배치 job step의 tasklet이나 itemReader, itemWriter에 붙는 어노테이션이다.
해당 어노테이션이 붙은 Bean은 Spring 컨테이너 실행시점에 생성되는 것이 아닌, 해당 Step이 실행되는 시점에 생성된다.
(Spring Bean의 기본 Scope는 singleton이기 때문에 생성시점은 중요하다.)

  • 사용이유
    : Spring Batch의 Job은 기본적으로 실행될 때, JobParameters가 함께 입력되어 실행된다. 이 때, Job Instance를 구분하는 기준은 JobParameters이고, 중복 JobParameters의 Job은 생성되지 않는다.

하지만 배치 Job Bean 생성 시점은 Spring Container 생성 시점이기 때문에, Bean 생성작업은 한번만 실행된다. 그렇기 때문에 실행 Job은 JobParameters로 구분할 수 있겠지만, 동작은 항상 동일할 것이다.

이러한 Batch의 맹점을 보완한 것이 @StepScope@JobScope이고, JobParameters활용 시에는 해당 어노테이션 활용이 병행되어야 한다.

어떻게 보완했나?

나도 역시 위에서 언급한 Batch의 맹점을 발견하였다.
해당 맹점은 위에서 언급했다시피 배치 job동작 시, 동일 file만 반복적으로 Read하는 현상이다.

해당 현상은 배치 job의 초기화가 Spring Container 생성 시점에 진행되기 때문에,
초기화 시점에 선정된 대상 file만 반복적으로 Read하는 것이라고 판단했다. (=> 더 알아봐야할 듯.)

해당 현상을 개선하기 위해 아래 코드처럼 @StepScope어노테이션을 선언하였다.

  • ResultReader.java
@Configuration
@RequiredArgsConstructor
public class InterfaceReader {

    @Bean
    @StepScope // 어노테이션 추가
    public <T> FlatFileItemReader<T> resultFlatFileItemReader() throws IOException {

        String targetFile = null;
        List<String> findFileList = new ArrayList<String>();
        String rootDir = "/save/";
        String fileNamePrefix = "INTERFACE_";
        String fileFullname = rootDir + fileNamePrefix;

        FlatFileItemReader<T> flatFileItemReader = new FlatFileItemReader<>();
        flatFileItemReader.setEncoding("UTF-8");

        try {

            // FileReader 설정
            setFileReaderConfiguration(Result.class, new ResultFieldSetMapper(), flatFileItemReader);

            Path dirPath = Paths.get(rootDir);
            Stream<Path> walk = Files.walk(dirPath, 1);
            List<Path> fileList = walk.collect(Collectors.toList());

            // Read 대상 파일 찾기
            for (Path path : fileList) {
                int extensionIdx = path.toString().lastIndexOf(".");
                if (path.toString().startsWith(fileFullname) && extensionIdx != -1 && !path.toString().substring(0, extensionIdx).endsWith("END")) {
                    targetFile = path.toString();
                    findFileList.add(path.toString());
                }
            }

            flatFileItemReader.setResource(new FileSystemResource(targetFile));

        }
        catch (NoSuchFileException e) {
            System.out.println(e.toString());
        }
        catch (NullPointerException e) {
            System.out.println(e.toString());
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }

        return flatFileItemReader;
    }

    private <T> void setFileReaderConfiguration(Class<T> c, FieldSetMapper<T> mapClass, FlatFileItemReader<T> flatFileItemReader) {

        ... (중략)

    }

}

@StepScope어노테이션을 위 코드처럼 선언하게 되면,
해당 Step이 실행될 때마다 Bean 생성 과정을 반복하게 되어 Read 대상파일을 갱신하게 된다.

리팩토링

File to DB 개발은 총 세가지 종류의 파일을 읽기 위해 시작되었다.
그리고 파일종류마다 각각의 Batch Job과 Step, Reader 등을 구성하여 기능을 구현하였다.

그러다보니 중복되는 부분이 많았다. ( ItemReader로직 동일 )
컨셉은 File을 Read해서 DB에 저장하는 것으로 동일하니, 어쩔 수 없는 부분이었고 공통로직을 모듈화시켜서 효율성을 증대시키고 싶었다.

ItemReader를 공통모듈 형태로 따로 빼고, 객체형태로 Parameter를 받아서 실행시키기 위해
아래와 같이 코드를 리팩토링 하였다.

  • application.yml
ENV_POSTGRESQL: 127.0.0.1
ENV_POSTGRESQL_PORT: 5432
ENV_POSTGRESQL_DB: batch
ENV_POSTGRESQL_SCHEMA: daewoong
POSTGRESQL_USERNAME: postgres
POSTGRESQL_PASSWORD: postgres

DIR_DELIMITER: /
SFTP_ROOT: /sftp/
SFTP_DEST1: /sftp/1/
SFTP_DEST2: /sftp/2/
SFTP_DEST3: /sftp/3/

server:
    port: 8080

spring:
    main:
        allow-bean-definition-overriding: true

    datasource:
        url: jdbc:postgresql://${ENV_POSTGRESQL}:${ENV_POSTGRESQL_PORT}/${ENV_POSTGRESQL_DB}
        username: ${POSTGRESQL_USERNAME}
        password: ${POSTGRESQL_PASSWORD}

    jpa:
        hibernate:
            ddl-auto: create
        database-platform: org.hibernate.dialect.PostgreSQLDialect
        properties:
            hibernate:
                format_sql: true
                show_sql: true

5개의 환경변수를 추가하였다. ( DIR_DELIMITER, SFTP_ROOT, SFTP_DEST1, SFTP_DEST2, SFTP_DEST3 )

  1. DIR_DELIMITER : 디렉토리 path 구분자, window와 linux의 형태가 다르기 때문에 따로 입력. 파일처리에 활용
  2. SFTP_ROOT : Read 대상의 파일들이 있는 path
  3. SFTP_DEST : Read 후, 파일들이 저장되어야할 path. 파일처리에 활용

  • ReaderRequestDto.java
@Component
@Data
public class ReaderRequestDto {

    private String rootDir;                // 파일 저장 경로
    private String destDir;                // END파일 저장 경로
    private String fileNamePrefix;        // 파일이름 접두사
    private String delimiter;            // 구분자 => 파일데이터 구분, 디렉토리 path 구분자와 혼동x
    private String[] columnNames;        // 컬럼명
    private String targetFile;            // 읽은 파일

}

위와 같이 공통 모듈의 파라미터로 활용할 객체를 선언하였다.

  1. fileNamePrefix : 각 종류의 파일은 접두사로 구분되기 때문에 해당 값을 활용하여 대상 파일을 탐색하였다.
  2. columnNames : FlatFileItemReader는 파일데이터를 컬럼명에 알맞게 매핑하기 위해 따로 컬럼명을 입력받아 설정해주어야 한다. 이를 위해 필요한 값이다.
  3. targetFile : job이 한번 실행될 때, read된 파일이다. 파일처리에 필요한 Listener에서 활용하기 위해 필요한 값이다.

  • CommonReader.java
@Component
@RequiredArgsConstructor
public class CommonReader {

    private final ReaderRequestDto readerRequestDto;

    public <T> FlatFileItemReader<T> commonFlatFileItemReader(Class<T> c, FieldSetMapper<T> mapClass throws IOException {

        String targetFile = null;
        List<String> findFileList = new ArrayList<String>();
        String rootDir = readerRequestDto.getRootDir();                    // parameter 입력
        String fileNamePrefix = readerRequestDto.getFileNamePrefix();    // parameter 입력
        String fileFullname = rootDir + fileNamePrefix;

        FlatFileItemReader<T> flatFileItemReader = new FlatFileItemReader<>();
        flatFileItemReader.setEncoding("UTF-8");

        try {

            // FileReader 설정
            setFileReaderConfiguration(Result.class, new ResultFieldSetMapper(), flatFileItemReader);

            Path dirPath = Paths.get(rootDir);
            Stream<Path> walk = Files.walk(dirPath, 1);
            List<Path> fileList = walk.collect(Collectors.toList());

            // Read 대상 파일 찾기
            for (Path path : fileList) {
                int extensionIdx = path.toString().lastIndexOf(".");
                if (path.toString().startsWith(fileFullname) && extensionIdx != -1 && !path.toString().substring(0, extensionIdx).endsWith("END")) {
                    targetFile = path.toString();
                    findFileList.add(path.toString());
                }
            }
            readerRequestDto.setTargetFile(targetFile);        // parameter 세팅

            flatFileItemReader.setResource(new FileSystemResource(targetFile));

        }
        catch (NoSuchFileException e) {
            System.out.println(e.toString());
        }
        catch (NullPointerException e) {
            System.out.println(e.toString());
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }

        return flatFileItemReader;

    }

    private <T> void setFileReaderConfiguration(Class<T> c, FieldSetMapper<T> mapClass, FlatFileItemReader<T> flatFileItemReader) {

        // File 내 구분자 설정
        DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer(readerRequestDto.getDelimiter());    // paramter 입력
        // 구분자 기준, 저장될 컬럼 이름 및 순서 지정
        delimitedLineTokenizer.setNames("NAME", "DATE", "SEQ", "STRING");

        // Mapper 설정
        DefaultLineMapper<T> defaultLineMapper = new DefaultLineMapper<>();
        BeanWrapperFieldSetMapper<T> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
        defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
        if (mapClass != null) {
            defaultLineMapper.setFieldSetMapper(mapClass);
        }
        else {
            beanWrapperFieldSetMapper.setTargetType(c);
            defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
        }

        flatFileItemReader.setLineMapper(defaultLineMapper);

    }

}

기존과 다르게 위 코드에서 보이는 것처럼 객체 parameter로부터 대상파일을 탐색하기 위한 root와 fileName정보를 입력받고,
file read가 끝나면 대상 파일을 다시 객체 parameter에 세팅해주는 로직이 추가되었다.

parameter를 입력받는 모듈로 구성하기 위해 @Component어노테이션을 활용하여 ItemReader가 아닌 별도 모듈형태로 Bean을 생성하도록 하였다.

  • Sftp1Reader.java
@Configuration
@RequiredArgsConstructor
public class Sftp1Reader {

    private final CommonReader commonReader;
    private final ReaderRequestDto readerRequestDto;

    @Value("${SFTP_ROOT}")
    private String rootDir;

    @Value("${SFTP_DEST1}")
    private String destDir;

    @Bean
    @StepScope
    public FlatFileItemReader<Result> resultFlatFileItemReader() throws IOException {

        String[] columnNames = {"NAME", "DATE", "SEQ", "STRING"};
        readerRequestDto.setRootDir(rootDir);
        readerRequestDto.setDestDir(destDir);
        readerRequestDto.setFileNamePrefix("INTERFACE_");
        readerRequestDto.setDelimiter("|");
        readerRequestDto.setColumnNames(columnNames);

        return commonReader.commonFlatFileItemReader(Result.class, new ResultFieldSetMapper());
    }

}

위와 같이 Step을 재구성하여 공통 모듈인 commonReader에 parameter형태로 필요한 값이 입력될 수 있도록 리팩토링하였다.

그 결과, ItemReader 로직 수정이 필요할 시 공통로직 수정을 통해 빠르게 반영하여 생산성을 제고할 수 있었다.

Read 파일 처리기능 추가

앞서 언급한 것처럼 Read된 파일들은 뒤에 _END를 붙여야 하고, 각각의 디렉토리에 이동시켜 보관해야 한다.
해당 기능은 StepExecutionListener를 활용하여 구현하였다.

StepExecutionListner

Spring Batch는 배치 job의 동작을 보조하기 위한 여러 종류의 Listener를 제공한다.

  1. JobExecutionListener : Job 단계 전/후에 동작하는 Listener
  2. StepExecutionListener : Step 단계 전/후에 동작하는 Listener
  3. ChunkListener : Chunk 단계 전/후에 동작하는 Listener
  4. ItemReadListener : ItemReader 단계 전/후에 동작하는 Listener
  5. ItemProcessorListener : ItemProcessor 단계 전/후에 동작하는 Listener
  6. ItemWriteListener : ItemWriter 단계 전/후에 동작하는 Listener
  7. SkipListener : Skip이 발생한 경우 동작하는 Listener
  8. RetryListener : Retry 전/후에 동작하는 Listener

File 데이터를 읽고 DB에 성공적으로 저장되면 파일처리를 해야하기 때문에,
StepExecutionListener를 활용하였고 ItemWriter 동작이 끝나면 실행될 수 있도록 아래와 같이 코드를 수정하였다.
( CommonStepExecutionListener DI추가 및 interfaceStep 메소드 수정 )

  • InterfaceFileReaderBatchJobConfig.java
@Configuration
@RequiredArgsConstructor
public class InterfaceFileReaderBatchJobConfig {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    private final InterfaceProcessor interfaceProcessor;
    private final InterfaceWriter interfaceWriter;

    private final CommonStepExecutionListener commonStepExecutionListener; // DI 추가

    private static final int chunkSize = 1000;

    @Bean
    public Job interfaceJob(Step interfaceStep) throws IOException {
        return jobBuilderFactory.get("interfaceJob")
                .start(interfaceStep)
                .build();
    }

    @Bean
    public Step interfaceStep(FlatFileItemReader<Result> resultFlatFileItemReader) throws IOException {
        return stepBuilderFactory.get("interfaceStep")
                .<Result, Result>chunk(chunkSize)
                .reader(resultFlatFileItemReader)
                .processor(interfaceProcessor)
                .writer(interfaceWriter)
                .listener(commonStepExecutionListener) // Listener 추가
                .build();
    }
}

그리고 아래와 같이 실질적인 파일처리를 위한 Listener로직을 구성하였다.

  • CommonStepExecutionListener.java
@Component
@RequiredArgsConstructor
public class CommonStepExecutionListener implements StepExecutionListener {

    private final ReaderRequestDto readerRequestDto;

    @Value("${DIR_DELIMITER}")
    private String dirDelimiter;

    @Override
    public void beforeStep(StepExecution stepExecution) {

    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {

        if (readerRequestDto.getTargetFile() == null) {        // Reader 동작 시, 세팅된 targetFile 값
            return stepExecution.getExitStatus();
        }

        String totalSourceName = readerRequestDto.getTargetFile();    // 처리 대상의 파일명
        String destDir = readerRequestDto.getDestDir();                // 처리 후 이동될 경로
        int extensionIdx = totalSourceName.lastIndexOf(".");        // 확장자 바로 이전의 인덱스 값 확인
        String sourceName = totalSourceName.subString(0, extensionIdx);    // 확장자 제외 파일명
        int fileNameIdx = sourceName.lastIndexOf(dirDelimiter);
        String fileName = sourceName.subString(fileNameIdx + 1, sourceName.length());    // 경로데이터 제외 파일명
        String extension = totalSourceName.substring(extensionIdx, totalSourceName.length());    // 확장자

        String destName = destDir + fileName + "_END" + extension;        // 최종 파일명 (목적지 경로 포함)

        try {
            Path source = Paths.get(totalSourceName);
            Files.move(source, source.resolveSibling(destName));    // source -> dest 파일 이동
        }
        catch (Exception e) {

        }

        return stepExecution.getExitStatus();
    }

}

이어서

이로써 Spring Batch의 기능적인 부분은 모두 구현하였다.

하지만 예상치 못한 오류를 대비해야 했다.
서버문제로 인해 해당 프로그램이 잘 동작하지 않으면, File은 처리되지 않고 지속적으로 쌓이기만 할 것이다.
그렇기 때문에, File처리를 위한 Job을 실행시키는 Scheduler를 관리자 임의대로 runtime환경에서 설정할 수 있어야 했다.

이를 위해 Scheduler 기능을 추가 구현하였다.
Spring Batch 외의 내용이지만, 다음 글에서 추가로 다룰 것이다.

반응형

+ Recent posts