반응형

신규 플젝에 투입되면서 공통 웹 개발파트를 맡게 되었다. 그놈의 웹개발은 그만하고 싶긴 한데, 하라면 해야지 뭐. 아무튼, 공통부라길래 그냥 공통 컴포넌트 정도 만지작거릴 줄 알았는데 그게 아니었고, 이번에 많이 배우는 듯 싶다.

 

서론 . tiles 추가하기에 앞서

현재 전자정부 프레임워크 4.2 버전을 사용중이다.

SI를 하건 뭘 하건 대부분의 웹 페이지는 다음과 같은 레이아웃 구조를 가지고 있다. 

 

저기서 상단의 Header , 좌측의 Nav, 하단의 Footer는 고정형 이미지이며 Body는 개발자들이 만들어내는 업무페이지라고 보면 된다. 처음에는 Header / Nav / Footer에 해당하는 JSP를 각각 만들고 Include하는 방식으로 생각했었는데 이거 아무리봐도 나이스하지가 않단말이다. 무슨 말이냐 하면, 

1
2
3
4
5
6
7
8
9
10
11
<div class="root-container">
    <%@ include file="/WEB-INF/jsp/commons/commLeft.jsp"%> <!-- 좌측 메뉴 -->
    <div class="container-wrap">
        <%@ include file="/WEB-INF/jsp/commons/commNav.jsp"%> <!--슬라이드 메뉴 -->
    </div>
    
    <!-- 요약을 많이 했지만 jsp 화면 컴포넌트가 여기들어감. -->
 
    <div class="footer-container">
    <%@ include file="/WEB-INF/jsp/commons/commFooter.jsp"%> <!-- 하단 푸터 -->
</div>
cs

 

 

이렇게 모든 공통 레이아웃들을 저렇게 주저리주저리 include해야 한다. 우리는 상단의 이미지와 같이 심플한 구조가 아니라 공통 헤더, 공통 타이틀, 공통 네비게이터, 공통 슬라이드 네비게이터, 공통 인피니트탭, 공통 푸터 까지 있으니 위와 같은 방식으로 include file하려면 여섯개는 더 있어야 한다.

 

가장 큰 문제는 무어냐, 저 포맷을 고대로 갖다 써서 수많은 div태그를 뚫고 업무 화면을 그려내면 좋으련만, 저건 아주 극도로 축약한 div 태그들일 뿐이고 실제로는 약 100여줄에 가까운 태그들이 난무를 하고 있다는거다.

그럼 당연히 개발자의 실수 하나로 레이아웃이 모조리 깨지기도 하고 가독성도 너무 구려서 개발 난이도가 올라간다. 이럴때 무엇을 쓴다? apache tiles다.

tiles를 적용함으로 인해 공통으로 사용하는 레이아웃은 그대로 공통부에서 처리하고 업무 화면을 지정한 div태그의 어딘가에 끼워넣는 개념이라고 보면 된다. 즉, tiles로 사전에 레이아웃을 설정하고 특정 영역만 호출하는 개념이다. 이마리야.

 

1. tiles 적용 - pom.xml

tiles를 적용하기에 앞서 기본적인 레이아웃에 대한 작업은 퍼블리셔분께서 작업해주셨다고 가정한다. 이것까지 하다간 난 죽을지도 모른다. 아무튼, pom.xml에 다음과 같이 추가했다. GPT는 몇개 안넣어줬는데 돌려보니 계속 오류가 나서, 보태보태 병에 걸려 저렇게까지 추가하고 성공함.

<dependency>
  <groupId>org.apache.tiles</groupId>
  <artifactId>tiles-core</artifactId>
  <version>3.0.8</version>
</dependency>
<dependency>
  <groupId>org.apache.tiles</groupId>
  <artifactId>tiles-jsp</artifactId>
  <version>3.0.8</version>
</dependency>
<dependency>
  <groupId>org.apache.tiles</groupId>
  <artifactId>tiles-servlet</artifactId>
  <version>3.0.8</version>
</dependency>
<dependency>
  <groupId>org.apache.tiles</groupId>
  <artifactId>tiles-api</artifactId>
  <version>3.0.8</version>
</dependency>
<dependency>
  <groupId>org.apache.tiles</groupId>
  <artifactId>tiles-el</artifactId>
  <version>3.0.8</version>
</dependency>

 

라이브러리들 이름이 참 쉬우니 대충 알아들을건데 뭐 tiles라이브러리 코어모듈, jsp 모듈, 서블릿 제어모듈, api 호출모듈, el 명령어 모듈 정도로 네이밍이 된 듯 싶다. 확실한것인지는 잘 몰루. 아무튼. 여기까지 하고 maven update~ 한다.

 

2. tiles.xml 설정

pom.xml에 라이브러리를 추가했다면 다음은 tiles.xml을 설정해준다. 공통 레이아웃을 무엇을 쓸지 선언하는 부분이라 보면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<tiles-definitions>
    <!--공통 레이아웃 정의 -->
    <definition name="tilesbase" template="/WEB-INF/jsp/commons/commLayout.jsp">
        <!--상단 헤더 -->
        <put-attributename="header" value="/WEB-INF/jsp/commons/commHeader.jsp"/>
        <!--좌측 메뉴 -->
        <put-attributename="left" value="/WEB-INF/jsp/commons/commLeft.jsp"/>
        <!--동적 바디 -->
        <put-attributename="body" value=""/>
        <!-- 하단 푸터 -->
        <put-attributename="footer" value="/WEB-INF/jsp/commons/commFooter.jsp"/>
    </definitions>
 
    <!--URL호출에 따른 동적호출부 1depth, 2depth,3depth -->
    <definition name="site/*" extends="tilesbase">
        <put-attribute name="body" value="/WEB-INF/jsp/{1}.jsp" />
    </definition>
    <definition name="site/*/*" extends="tilesbase">
        <put-attribute name="body" value="/WEB-INF/jsp/{1}/{2}.jsp" />
    </definition>
    <definition name="site/*/*/*" extends="tilesbase">
        <put-attribute name="body" value="/WEB-INF/jsp/{1}/{2}/{3}.jsp" />
    </definition>
</tiles-definitions>
cs

 

간단히 설명하자면 공통 레이아웃을 commLayout.jsp로, tilesbase라는 이름으로 정의하고

commHeader.jsp를 header , commLeft.jsp는 left , commFooter.jsp는 footer로 태그로 정의하고

하단에는 3depth url 규격에 맞춰서 호출한다는 의미이다. 

 

현재 우리 시스템은 3depth url규격으로 되어있다. localhost:8080/test/test/test.jsp 라는 형태.

그래서 site/*/*/* 로만 정의해도 문제 없으나, 일단은 혹시몰라 3depth를 모두 개별로 정의했다.

tilesbase, tiles.xml 및 site/* 등등 모든것들은 개발에 맞춰 설정하면 된다.

 

3. commLayout.jsp생성

예시로 맨 위의 이미지와 같이 첨부하는 방법을 들어줄 것이다. 기본 공통 레이아웃은 div태그별로 잘게 잘 쪼개서 commHeader.jsp, commLeft.jsp, commFooter.jsp와 같은 형태로 만들어준다. 

저 comm시리즈와 함께 우리가 업무에서 만들 것들을 조립하는 큰 판을 commLayout.jsp라고 칭한다. 이 commLayout.jsp에서 블록을 조립하듯 퍼블리셔분께서 작업해주신 div태그 class별로 조립을 하는거다.

 

노파심에 덧붙여(미래의 이 작업을 할 내게) jsp의 div class 별로 큰 구획들이 잡혀있으니 이것들을 잘게 쪼개면 된다.

큰 class 별로 쪼개서 commHeader, commLeft, commFooter 등등을 만들어 jsp로 만들어두고, commLayout 에서 tiles 에서 설정한 각각의 jsp들을 조립하는 형태다. 다음의 소스코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(중략)
 
<div class="wrapper">
    <!--공통 Header -->
    <tiles:insertAttribute name="header" />
 
    <div class="root-container">
        <div class="left-wrap">
            <tiles:insertAttribute name="left" />
        </div>
        <div class="contents-wrap">
            <tiles:insertAttribute name="body" />
            <tiles:insertAttribute name="footer" />
        </div>    
    </div>
</div>
cs

commLayout.jsp 에서 tiles태그를 사용하여 tiles.xml에서 정의한 jsp들을 include하는 형태이다.

예제 소스코드라서 간략하게 되어있지만, 실제로는 div class의 구조가 복잡하느라 퍼블리셔 작업물을 몇 번이나 확인했는지 모르겠다.

일단 여기까지 설정을 온전하게 잘 했다면 큰 고비는 넘긴것이다.

 

4. dispatcher-servlet.xml 설정

dispatcher-servlet에서는 spring mvc 구조에서 모든 클라이언트의 요청을 받아 처리하는 부분으로, 일반적으로 jsp view resolver로 설정이 되어있다.

그런데 이렇게 설정했다가는 호출하는 jsp가 먼저 먹어버리니까, 우선순위를 tiles 로 먹도록 설정해야한다. 다음의 설정을 보자. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(중략)
 
<!-- Tiles 설정 -->
<bean id="tilesConfigurer"
    class="org.springframework.web.servlet.view.tiles3.TilesConfigurer"
    p:checkRefresh ="true">
    <property name="definitions">
        <list>
            <value>/WEB-INF/tiles/tiles.xml</value>
        </list>
    </property>
</bean>
 
<!-- Tiles 설정 -->
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
    p:viewClass="org.springframework.web.servlet.view.tiles3.TilesView"
    p:order="0" />
 
<!--기존의 ViewResolver 설정 - order를 후순위(3)로 설정 -->
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
    p:viewClass="org.springframework.web.servlet.view.JstlView"
    p:prefix="/WEB-INF/jsp/"
    p:suffix=".jsp"
    p:order="3" />
cs

 

dispatcher-servlet에 tiles관련 설정을 넣어준다. 동시에, 모든 view호출은 tiles 우선으로 처리하도록 (그래야 레이아웃이 먹으니) p:order를 0으로 주고, 기존의 view Resolver설정을 유지함과 동시에 후순위(나는 3순위로)로 설정한다.

이게 끝이 아니다. 우리는 Controller 에서 url을 호출하는 부분도 수정해야 한다.

 

 

5. 공통 컨트롤러 수정 

나는 위에서 site/*/*/* 형태로 설정했다. 이 말은, ModelAndView 에서 return 하는 uri에 대해 tiles definition이 가로채서 WEB-INF/jsp/{1}/{2}/{3}.jsp 에 있는 jsp를 리턴해준다는 의미이다.

기존에 설정된 viewResolver도 동일하다. 하지만 내가 설정한 것은 site/를 반환하도록 되어있으니, 공통부 ModelAndView에서도 site/ 형태로 반환해야 한다.

 

그냥 별거 없이, ModelAndView return시 "site/"를 StringBuilder 에서 append해주면 된다. 이건 개발자 구현 나름이니까.

그래서 최종 형태는 다음과 같아야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public ModelAndView commonVie(HttpServletRequestreq, HttpServletResponse resp
        , @PathVariable String oneDepth
        , @PathVariable String twoDepth
        , @PathVariable String threeDepth) throws Exception{
    
    // 중략
 
    StringBuilder pagePath = new StringBuilder();
    pagePath.append("site/")
        .append(oneDepth)
        .append("/")
        .append(twoDepth)
        .append("/")
        .append(threeDepth);
    
    // site/oneDepth/twoDepth/threeDepth 형태로 반환
    ModelAndView mav = new ModelAndView(pagePath.toString());
    
    return mav;
}
cs

 

이렇게 해줘야 site/ 형태로 url을 리턴하고, tilesResolver가 캐치해서 tiles.xml의 구조로 리턴하게 될 것이다.

 

그런데 왜 site/형태를 쓸까?

site/ 형태의 url을 쓰지 않으면 모든 경로는 /one/two/three형태로 리턴이 될 것이고, 모든 페이지에 대해 tiles 가 적용이 될 것이다. 이 말인 즉, /login , /error와 같은 수많은 퍼블릭 페이지들도 tiles영향을 받게 된다는 점이다.

 

그래서 퍼블릭 페이지와의 구분을 위해 내부 페이지들의 구분자 역할을 하는 개념이라 보면 된다.

 

일단 여기까지, 이 설정을 내부망에서 한다고 얼마나 삽질했던지. 아휴 힘들었다. 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

회사에서 자체적으로 제작한 Spring 기반의 프레임워크/튜닝 Eclipse로 개발을 할 일이 생겼는데, 쿼리 작성하는 xml이 뭔가 좀... 다르다? 

대충 내가 지금까지 써온 xml 형태랑 다르다. 

 

쓰다보니 가장 큰 문제가 바로 Ctrl-z / Ctrl-y를 이용한 되돌리기 기능이 안먹힌다는 것인데, 이러다보니 뭐 하나 잘못 지워버리면 되돌리기가 안되서 -_-;; 아주 난감했다. 어떻게 이 상태로 한 달을 개발했는지 모르겠는데.

 

아무튼 구글링을 대충 Eclipse xml 되돌리기 로 검색해보니 원인을 찾을 수 있었다.

원인은 '기본 xml 열기 속성이 XML (Spring XML Config File)로 설정' 되어 있었던 것. 이것을 XML Editor로 변경해주니 아주 잘 동작한다.

 

그래 이렇게 등록되어야 tag 자동완성도 되고 좋지. ㅋㅋㅋ

 

설정 방법은 다음과 같다. 

 

Windows -> Preferences -> General -> Editors -> File Associations -> *.xml -> XML Editor 선택 후, Default 클릭.

이렇게 설정하면 XML 에디터로 열려 태그 자동완성부터 시작해서 안먹히던 되돌리기/복구하기도 잘 되고. 코드하이라이터(?)도 잘 보이고 아무튼 좋다. ㅋㅋㅋ 아 진짜 별놈의 설정이 다 있었네. ㅋㅋㅋㅋ 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

갑자기 몇년만에 웹 개발을 지원요청받아서 투입되었다. 거기에 생전 처음보는 울 회사 자체 프레임워크로 커스터마이징된 SpringBoot 기반 프레임워크;; 아 이건 또 뭐야. 대체.

 

아무튼 개발 잘 하고 있는데 엑셀 파싱하고 sysout 으로 출력하는데 한글 인코딩이 깨져서 보인다. 다른 한글들은 잘 출력되는걸 봐서 뭔가 문제가 있는것 같은데, 일단 급한대로 한글이 잘 나오는데만 초점을 두고 GPT 에게 물어보았다.

 

DATA ROW : 1 : �� ,,,,,, : [Lv, ����ID, ��������, ��ȹ, ����, ����, �����, ����, ��ȹ������, ��ȹ������, ����������, ����������, ���⹰, ���, �޼���]
DATA ROW : 2 : �� ,,,,,, : [4, PJ_001_IFT_001_002_001, �����û, 92.86%, 90.91%, ������, , , 2024-10-01, 2024-10-15, 2024-10-04, , , ������?����, ]

이렇게 보이는 상황 -_-;

디버깅 모드로 찍어보니 오브젝트 내의 데이터들은 모두 한글로 잘 가져온 상황. 그러니까 파싱문제는 아니다, 이마리야.

 

일단은 Run Configuration의 VM Argument를 -Dfile.encoding=UTF-8 로 준다거나 하는 설정은 싹 다 줘봤고, windows - preference 의 text encoding 같은것도 UTF-8로 줬는데 문제가 해결되지는 않았다.

다른방법을 더 강구하니 이거 함 써보라고 던져준다.

 

1
2
3
4
5
try {
    System.setOut(new PrintStream(System.outtrue"UTF-8"));
catch (UnsupportedEncodingException e) {
        e.printStackTrace();
}
cs

 

일단 이렇게 하니까 잘 나온다. 원인 분석까지는 시간이 조금 걸릴듯. 이런건 또 생전 첨보네 ㅡㅡ;; 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이번 요구사항 중 특이한 요구사항이 있었다. 사실 특이... 까진 아니긴 한데, 설계 하다보니 이건 특이할 수 밖에 없는 상황이 생겨버림. 대략적인 설명은 다음과 같다.

기본전제 - FTP 업로드/다운로드

1. 한 명의 사용자가 여러개의 데이터를 FTP 업로드/다운로드 신청을 할 수 있다
2. 여러명의 사용자가 1번의 요구사항과 같이 신청할 수 있다
3. 한 명의 사용자는 한 건의 FTP 사용량만 가진다. 즉, 한 명이 10건을 신청해도 1건씩 업로드/다운로드가 되는 형식
4. 5명의 사용자가 각각 5건의 FTP 데이터 전송 요청을 하는 경우, 5건의 FTP 전송만 생성된다
5. 일반적으로 전송되는 데이터의 사이즈는 최소 수십 GB를 기본으로 한다 (FTP 속도가 오래걸림)

 

원래대로라면 그냥 신청을 하는 WEB단에서 API 서버로 FTP 전송 요청을 하면 수행하는 구조로 생각했는데, 기본적인 전송 단위가 몇기가 단위이다보니 자칫하다가는 Web Session Timeout이 발생할 수도 있는 상황. 그리고 3번과 같은 요구사항에 의해 결국 FTP 전송과 관련된 DB Table을 설계하고, API 서버에서는 해당 Table을 주기적(Scheduled)으로 Select 하면서 전송건이 있는지를 체크하는 방식으로 설계를 했다.

그리고 4번의 전송건과 관련해서 결국 Async 방식으로 FTP 호출을 하는 것으로 구현했다. 즉, 5명의 사용자가 각기 5개의 데이터를 전송요청 하는 경우, Async Thread에 의해 5개의 FTP 세션을 생성하고 전송할 것이다.

 

그런데 여기서 문제가 발생한다.

가령 DB 호출 스케쥴러가 10분에 한 번씩 작동한다고 가정하자. 첫 구동시 A 데이터(사이즈 100기가)가 Select 되어 FTP 전송을 하는데, 대략적으로 100분이 걸린다고 할 때, 전송이 완료되는 시점까지 스케쥴러는 이 데이터를 계속 호출할 것이다. 단순하게 DB 쿼리로 걸러내면 되지 않을까? 라고 생각했으나, DB 쿼리로 걸러낸다 하더라도 내부적으로 방지하는 로직을 구현해야 할 것으로 판단했다. 

 

중복실행 방지 Async를 위해  제미니에게 물어보니, Async Queue를 관리하는 로직을 구현해주었고, 사용법이 쉬워 적용해보았더니 아주 잘 동작하게 되었다. 이하는 해당 로직의 간단한 소스코드와 예제이다.

 

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 java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
 
@Component
public class QueueCache {
    private static final QueueCache instance = new QueueCache();
    private final ConcurrentHashMap<String, Boolean> cache = new ConcurrentHashMap<>();
 
    private QueueCache() {
    }
 
    public static QueueCache getInstance() {
        return instance;
    }
    
    // key가 Cache에 있는지 확인
    public boolean isReq(String key) {
        return cache.containsKey(key);
    }
    
    // 신규 작업시 Key를 Cache에 등록
    public void putReq(String key) {
        cache.put(key, true);
    }
 
    // 작업 완료시 Key를 Cache에서 삭제
    public void removeReq(String key) {
        cache.remove(key);
    }
}
cs

 

싱글톤 패턴으로 모든 클래스에서 단 하나의 인스턴스만 생성하는 구조로, 여러개의 쓰레드가 접근하여도 단 하나만의 키를 관리하는 방식으로 구현되어있다.

주석에 달린 설명과 같이 사용법이 크게 어렵지는 않다. 나의 경우에는 아래와 같은 방식으로 로그가 발생하였다.

1
2
3
4
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-1
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-2
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-3
[scheduling-1- INFO k.a.testAPI.common.testController - === Queue 에 있음 - USERKEY-4
cs

 

USERKEY-1~4 까지 Unique 한 값으로 QueueCache에 등록하고, 모든 로직을 수행하기 직전에 isReq(USERKEY-1)하는 식으로 Queue에 등록되어있는지를 확인한다. 없는 경우, Queue에 등록(putReq)을 한 후, 모든 작업이 종료되거나 또는 exception이 발생하는 경우 removeReq를 통해 Queue 에서 삭제를 한다.

 

이로인해 수시간 동안 동작하는 경우에도 안정적으로 Async FTP 전송을 수행할 수 있었다. 메데타시 메데타시.

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

아니 왜 응답 날짜값을 UTC로 주는지 모르겠다 증말. 이런건 그냥 심플하게 심플데이터 포맷으로 yyyyMMddHHmmssSSS로 찍어주면 어디 덧나나? 와 진짜. 아무튼 주는대로 받아먹어야하니 대충 정리해보았다.

 

날짜 형식은 다음과 같다.

2024-06-30T06:42:02.148+00:00

 

대충 찾아보니 UTC 형식의 DateTime으로, Simpledateformat을 기본으로 쓰는 내 시스템과는 근본적으로 달라서 날짜비교나 뭐 그런 기본적인 펑션이 하나도 먹지 않는다. 그래서 변환해서 DB에 넣고 꺼내기로 함.

 

java 1.8, SpringBoot 2.7.8 에서 실행했는데 이정도는 딱히 뭐 버전을 탈 일은 없겠지. 아무튼 잘 동작한다.

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
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Date;
import java.util.TimeZone;
 
 
/**
 * UTC To SimpleDateFormat
 */
public static String utcToSimpleDateFormat(String utcTime) {
    String simpleDateResult = "";
    
    // UTC 형식을 설정함
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
    
    Date utcDate;
    try {
        utcDate = sdf.parse(utcTime);
        Instant utcInstant = utcDate.toInstant();
        
        // Asia/Seoul 로 타임존 설정, 대소문자에 유의
        ZoneId seoulZone = ZoneId.of("Asia/Seoul");
        Instant seoulInstant = utcInstant.atZone(seoulZone).toInstant();
        
        // 변경할 시간 양식을 설정해주자
        SimpleDateFormat resultStr = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        resultStr.setTimeZone(TimeZone.getTimeZone(seoulZone));
        
        simpleDateResult = resultStr.format(Date.from(seoulInstant));
    } catch (Exception e) {
        e.getMessage();
    }
    
    return simpleDateResult;
}
cs

 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이번엔 외부시스템과 인터페이스 통신을 하는데, SSL 통신을 한다. 근데 뭐 고객사에서는 SSL 통신을 하는데 인증서도 안주고 도메인으로 통신하는 것도 아닌 IP 통신인지라 당연히 PKIX 오류가 발생하기 마련.

이 오류는 SSL 통신시 인증서 검증에서 실패했다는 뜻으로, 우리가 흔히 웹 브라우저로 인터넷 하다가도 '신뢰 할 수 없는 인증서입니다~ 무시하고 계속할래~?' 하는 식으로 묻는것도 이와 같은 맥락이라고 볼 수 있다. 

curl 에서는 -k 옵션 하나로 간단히 무시할 수 있는데, JAVA 에서는 쉽지 않았다. 이하는 try catch 부분.

 

java 1.8 및 SpringBoot 2.7.8 에서 수행했다.

 

오류는 다음과 같다.

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 

대충 curl 호출부는 다음과 같다. 이렇게 호출하면 성공한다.

curl -H 'Content-Type: application/json' -H 'Authrization: Basic toke' -k -x POST 'https://111.111.111.111:443/test/v1/auth/token' ;

 

curl 호출시 -k 옵션은 인증서 검증 우회하겠다는 의미이다.

 

이하는 java 소스코드.

전반적으로 httpURLConnection과 같은 양상을 띄지만 SSL 통신 설정부라거나, 기타 잡다한 설정들이 더 추가가 되어있다.

 

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.test.common.utils;
 
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;
 
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
 
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class HttpsURLConnector {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public JSONObject postURL ( String urlData , Map<String, Object> headers , Map<String, Object> param) {
        JSONObject jsonResult = new JSONObject();
        
        //Http 요청시 필요한 URL 주소의 변수 선언
        String totalUrl = "";
        totalUrl = urlData.trim().toString();
        
        // http 통신 객체 선언
        URL url = null;
        HttpsURLConnection httpConn = null;
        
        // http 통신 응답에 대한 변수
        String resData = "";
        BufferedReader buff = null;
        StringBuffer strBuff = null;
        
        // 호출 결과값에 대한 변수
        String returnData = "";
        try {
            ObjectMapper mapper = new ObjectMapper();
            String paramData = mapper.writeValueAsString(param);
            
            // url.openConnection을 시도하기 전에 아래 설정을 완료한다
            TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
 
                public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                }
 
                public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                }
            } };
            
            SSLContext sslContext;
            sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustAllCerts, null);
            
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
            
            // 파라미터로 들어온 url을 사용하여 connection 시도
            url = new URL(totalUrl);
            httpConn = (HttpsURLConnection) url.openConnection();
            httpConn.setHostnameVerifier(new HostnameVerifier( ) {
                
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    // return true로 해주어야 모든 SSL 검증을 무시함
                    return true;
                }
            });
            
            // http 요청 설정
            httpConn.setRequestMethod("POST");
            httpConn.setRequestProperty("Content-Type""application/json; utf-8");
            httpConn.setRequestProperty("Accept""application/json");
            
            // header 파라미터 설정
            if ( headers != null ) {
                for ( Map.Entry<String, Object> header : headers.entrySet()) {
                    httpConn.setRequestProperty(header.getKey(), header.getValue().toString());
                }
            }
            
            // Body부 생성 - doOutPut을 true로 주어야함
            httpConn.setDoOutput(true);
            try (OutputStream os = httpConn.getOutputStream() ){
                byte requestData[] = paramData.getBytes("utf-8");
                os.write(requestData);
                os.close();
            } catch ( Exception e) {
                logger.error("== OutputStream ERROR ::::: " + e.getMessage());
            }
            
            //커넥션~
            httpConn.connect();
            
            // 응답에 대한 데이터를 Buffer로 받아 처리
            buff = new BufferedReader(new InputStreamReader(httpConn.getInputStream(), "UTF-8"));
            strBuff = new StringBuffer();
            while ( ( resData = buff.readLine() ) != null ) {
                strBuff.append(resData); // StringBuffer에 응답받은 데이터를 순차적으로 저장함
            }
            
            returnData = strBuff.toString();
                
            //https 응답코드
            String resCode = String.valueOf(httpConn.getResponseCode());
            JSONParser parser = new JSONParser();
            Object obj = new Object();
            try {
                obj = parser.parse(strBuff.toString());
            } catch (Exception e) {
                logger.error("StringBuff Parser ERROR ::::: " + e.getMessage());
            }
            jsonResult = (JSONObject) obj;
        } catch (Exception e) {
            logger.error("TOTAL Error ::::: " + e.getMessage());
        }
        
        return jsonResult;
    }
}
cs

 

TrustAllCerts 부분과 setHostnameVerifier 부분을 잘 염두에 두면 큰 문제 없이 성공할 수 있다.

가장 중요한 부분은 url.openconnection  하기 전에 trustAllCerts를 선언해주어야 한다는 점이었다....

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

sftp를 활용할 프로젝트가 생겼다. 대충 개발망에서는 라이브러리가 없으니까, 외부 개인 놋북에서 샘플소스코드를 작성 하고 라이브러리를 반입하기로 했다. 이하는 대충 짜보고 찾아보고 제미니에게 물어본 소스코드.

 

Springboot 2.7.8, jcraft jsch 라이브러리는 0.1.55 버전 (0.1.54 버전도 동작됨을 확인함) 이다.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>
cs

 

1. SftpUtil.java

유틸과 같이 target Path , target FileName 정도만 입력받으려고 구성했다. 어차피 sftp 접속정보는 application.properties에 작성되어있을거니까.

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.test.common.utils;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
 
public class SftpUtil {
    private static final String SESSION_CONFIG_STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
    private static final Logger logger = LoggerFactory.getLogger(SftpUtil.class);
 
    private String host;
    private String username;
    private String password;
    
    private int port;
    private int timeout = 15000;
 
    public SftpUtil(String host, String username, String password, String port) {
        this.host = host;
        this.username = username;
        this.password = password;
        this.port = Integer.parseInt(port);
    }
 
    private ChannelSftp createSftp() throws Exception {
        JSch jsch = new JSch();
 
        Session session = createSession(jsch, host, username, port);
        session.setPassword(password);
        session.connect(timeout);
 
        Channel channel = session.openChannel("sftp");
        channel.connect(timeout);
 
        return (ChannelSftp) channel;
    }
 
    private Session createSession(JSch jsch, String host, String username, int port) throws Exception {
        Session session = null;
 
        if (port <= 0) {
            session = jsch.getSession(username, host);
        } else {
            session = jsch.getSession(username, host, port);
        }
 
        session.setConfig(SESSION_CONFIG_STRICT_HOST_KEY_CHECKING, "no");
        logger.info("===== session ::::: " + session);
 
        return session;
    }
 
    private void disconnect(ChannelSftp sftp) {
        try {
            if (sftp != null) {
                if (sftp.isConnected()) {
                    sftp.disconnect();
                } else if (sftp.isClosed()) {
                }
                if (null != sftp.getSession()) {
                    sftp.getSession().disconnect();
                }
            }
        } catch (JSchException e) {
            e.printStackTrace();
        }
    }
 
    // Rest에서 호출함
    public boolean uploadFile(String targetPath, String fileName, File file) throws Exception {
        FileInputStream fis = new FileInputStream(file);
        ChannelSftp sftp = this.createSftp();
        try {
            sftp.cd(targetPath);
            sftp.put(fis, fileName);
            return true;
        } catch (Exception e) {
            logger.debug("==== exception ::::: " + e.getMessage());
            throw new Exception("Upload File failure");
        } finally {
            this.disconnect(sftp);
        }
    }
 
    // Rest에서 호출함
    public boolean downloadFile(String remoteFilePath, String localSavePath) throws Exception {
        ChannelSftp sftp = this.createSftp();
        try {
            sftp.get(remoteFilePath, localSavePath);
            return true;
        } catch (Exception e) {
            logger.debug("==== exception ::::: " + e.getMessage());
            throw new Exception("Download File failure");
        } finally {
            this.disconnect(sftp);
        }
    }
 
}
cs

 

소스코드가 좀 길기는 한데, 어차피 Upload / Download는 공통적인 connection / disconnection 과정을 거치기에 상관은 없다.

 

호출부는 다음과 같다.

 

2. Rest 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
28
29
@PostMapping("/testSftpUpload")
public String testSftpUpload(@RequestBody String a) {
    String result = "";
    
    SftpUtil ftp = new SftpUtil( sftpHost, sftpUsername, sftpPassword, sftpPort );
    File file = new File("C:\\Users\\KimFish\\Desktop\\TEST.csv");
    try {
        ftp.uploadFile("/C:/sftp""1234.csv", file);
    } catch (Exception e) {            
        e.printStackTrace();
    }
    
    return result;        
}
 
@PostMapping("/testSftpDownload")
public String testSftpDownload(@RequestBody String a) {
    String result = "";
    
    SftpUtil ftp = new SftpUtil( sftpHost, sftpUsername, sftpPassword, sftpPort );
    File file = new File("C:\\Users\\KimFish\\Desktop\\TEST.csv");
    try {
        ftp.downloadFile("/C:/sftp/1234.csv""/C:/Users/KimFish/Desktop/test1234.csv");
    } catch (Exception e) {            
        e.printStackTrace();
    }
    
    return result;        
}
cs

 

이정도면 충분한듯.

각각의 호출부는 host , port , userid , userpw 모두 입력받고, 메서드에 따라 파일 및 경로 등을 같이 인자값으로 던지게 된다. 일단은 이정도로 되었다... 

 

 

*** 알아둘 점 ***

리눅스에서 디렉토리 구분자는 / 를 사용하는데 반해, 윈도우에서는 디렉토리 구분자를 \를 써야 한다.

그런데 ftp(sftp포함)에서는 무조건 /를 사용해야하니 저렇게 작성된 것. 그에 비해 File 메서드 같은 경우는 Windows OS에서 구동되는 자바 프로그램이기 때문에 \ 를 사용함을 볼 수 있다.

이렇게 또 하나 배우는구만... 어차피 리눅스 드가면 슬래쉬 쓸거...

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

이거 뭐라 오류가 난 것도 좀 거시기 하고 해서 쓰기가 애매하긴 한데, 아무튼.

Springboot 2.7.8 버전에서 Controller > Service > ServiceImpl > mapper > sql 로 이루어지는 과정에 있어 Return VO가 null 인 것을 확인했다.

 

쿼리도 그렇고 VO도 그렇고 아무런 문제가 없는것임에도 불구하고 도저히 뭐가 문제인지를 모르겠어서, sql에서 resultType을 VO가 아닌 java.util.hashMap 으로 받아보았더니 정상적으로 DB 쿼리가 조회되는데, Key 값이 스네이크 케이스로 들어온 것을 확인했다. 이러니까 카멜케이스 기반으로 작성된 vo에 매핑이 안되어서 null이 뜨는거지.

 

그런데 이게 대체 왜 지금와서 문제지? 지금까지 수행해온 프로젝트에서는 이거 다 매핑이 되었는데? 싶었다.

대충 찾아보니 mybatis 에서 이걸 옵션값으로 넣어주어야 한다는데, 내가 처음부터 밑바닥부터 세팅한 프로젝트는 지금이 처음이라서 그런가 싶었다.

 

아무튼, application-local.properties에 아래와 같이 넣어주니 스네이크 케이스의 컬럼값이 카멜케이스의 vo 값에 잘 넣은것을 확인할 수 있었다.

1
2
3
 
mybatis.configuration.map-underscore-to-camel-case=true
 
cs

 

와 진짜 별거 아닌걸로 30분은 날린 것 같네. 지금까지는 어디선가 이 옵션을 누가 넣어줬던걸 썼나... 

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

http request logging이라고 표현하는게 옳은것인지는 잘 모르겠지만 아무튼, API 인터페이스 서버를 개발하면서 요청시스템코드/요청자 아이디/응답시간/요청URI 와 같은 것들만을 간단하게 로깅해야하는일이 생겼다.

과거 다른 프로젝트에서 해둔 게 있었는데 전자정부 기반(이라고 해봤자 사실 스프링이랑 차이가 없음)이라, 조금 손보고 해서 SpringBoot 2.7.8 버전에 맞게 수정했다.

 

이것만 해서는 되지 않고, 기존에는 직접 해당 interceptor 에서 log파일까지 생성해서 append 하는 방식이었는데, 나는 이걸 logback에서 로깅하는 방식으로 수정해보았다. 로컬 개발환경에서는 잘 돌아가지만, 개발서버에서는 DispatcherServlet도 같이 로깅되는 문제가 있기는 하다. 이거에 대해 수정하는 건 좀 나중으로 미루고, 일단은 까먹기 전에 기록을 먼저 하는 걸로 하자.

 

1. LoggingInterceptor.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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.test.common.config;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
 
@Component
public class LoggerInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(LoggerInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res , Object handler) throws Exception {
        System.out.println("=== LoggerInterceptor ===");
        String contextPath = req.getContextPath();
        String requestUrl = req.getRequestURI().substring(contextPath.length());
        
        // 응답시간 체크를 위한 선언
        long startTime = System.currentTimeMillis();
        req.setAttribute("__start__time",  startTime);
        req.setAttribute("__call__time", getTodayStr("yyyyMMddHHmmss"));
        
        
        return true;
    }
    
    /** 
     * 로그 예시 
     * SYSTEM_CODE,yyyyMMddHHmmss,/test/api.do,00,9999,CALL_SYS_CD,CALL_USER,0:0:0:0:0:0:1
     */
    @Override
    public void postHandle(HttpServletRequest req , HttpServletResponse res , Object handler , ModelAndView modelAndView) throws Exception {
        long startTime = (long)req.getAttribute("__start__time");
        long endTime = System.currentTimeMillis();
        long resTime = endTime - startTime;         // 응답시간 계산
        
        // 호출일시
        String callTime = (String)req.getAttribute("__call__time");
        
        // 호출 URI
        String contextPath = req.getContextPath();
        String requestUrl = req.getRequestURI().substring(contextPath.length());
        
        // 호출 시스템 및 사용자 , api 통신시 http header에 세팅해야 함
        String callSysCd = req.getHeader("callSysCd");
        String callUser = req.getHeader("callUser");
        
        StringBuffer logSb = new StringBuffer();
        logSb.append("SYSTEM_CODE");
        logSb.append("," + callTime);
        logSb.append("," + requestUrl);
        logSb.append("," + "00");
        logSb.append("," + resTime);
        logSb.append("," + callSysCd);
        logSb.append("," + callUser);
        logSb.append("," + getClientIP(req));
        
        logger.info(logSb.toString());
    }
    
    
    /**
     * IP 가져오기
     */
    private static String getClientIP(HttpServletRequest req) {
        String ip = req.getHeader("X-Forwarded-For");
        
        if ( ip == null ) 
            ip = req.getHeader("Proxy-Client-IP");
        
        if ( ip == null ) 
            ip = req.getHeader("WL-Proxy-Client-IP");
        
        if ( ip == null ) 
            ip = req.getHeader("HTTP_CLIENT_IP");
        
        if ( ip == null ) 
            ip = req.getHeader("HTTP_X_FORWARDED_FOR");
        
        if ( ip == null ) 
            ip = req.getRemoteAddr();
        
        return ip;
    }
    
    // 오늘 날짜 String 리턴 
    private static String getTodayStr ( String pattern ) {
        if ( StringUtils.isEmpty( pattern ) ) {
            pattern = "yyyyMMdd";
        }
        
        SimpleDateFormat simpleDate = new SimpleDateFormat ( pattern );
        Calendar c1 = Calendar.getInstance();
        String strToday = simpleDate.format(c1.getTime());
        
        return strToday;
    }
}
 
cs

 

엄청 길어보이지만 사실 별거 아닌 소스이다. 

URL HttpServletRequest가 올 때 preHandle이 먼저 가로채서 적당한 데이터들을 가공해서 넣어준 후, URL에 매핑된 controller 가 실행된 후, postHandle이 가로채서 preHandle에 넣어준 데이터들을 빼내어 로그형태를 가공한다.

 

간단히 정리하자면 다음과 같다

(Client) Http URL 요청(Httprequest) -> (서버) Interface의 preHandle -> (서버) URL 매핑된 controller 로직 수행 -> (서버) client 응답 전 Interface의 postHandle -> (Client) Http URL 응답(HttpResponse)

 

그럼 이걸 어떻게 logback에서 설정했냐, 하면 다음과 같다.

 

2.WebConfig.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
package com.test.common.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    HeaderInterceptor headerIntercepter;
    
    @Autowired
    LoggerInterceptor loggerInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        System.out.println("===== ===== WebMvcConfig ===== =====");
        // Header에 저장된 시스템 코드를 체크하기 위해 headerInterceptor를 add 해준다
        registry.addInterceptor(headerIntercepter).addPathPatterns("/test/**");
        // HttpRequest Logging을 위해 loggerInterceptor를 add 해준다.
        registry.addInterceptor(loggerInterceptor).addPathPatterns("/**");
    }
}
cs

logback을 설정하기 전, WebMvcConfig를 만들고 여기서 interceptor를 정의해주어야 한다.

23번째 줄이 LoggerInterceptor를 정의해주는 부분이다.

 

3. logback-spring.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <property name="LOG_NAME" value="SampleProject" />
    <property name="LOG_HOME" value="/sw/apiServer/logs" />
    <property name="LOGBACK_HOME" value="/sw/apiServer/logs/old" />
    <property name="InterceptorLogPattern" value ="%msg %n" />
    
    <!--  개발툴 콘솔창 설정 -->
    <appender name ="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{0}:%line] - %msg %n</Pattern>
        </encoder>
    </appender>
    
    <!-- LoggerInterceptor 로그 파일 -->
    <appender name="INTERCEPTOR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <Append>true</Append>
        <file>${LOG_HOME}/insLog/${LOG_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGBACK_HOME}/insLog/${LOG_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
            <maxHistory>365</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <encoder>
            <Pattern>${InterceptorLogPattern}</Pattern>
        </encoder>
    </appender>
    
    <root leve="WARN">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="INTERCEPTOR"/>
    </root>
    
    <!-- 로그 대상 패키지 지정 -->
    <logger name="com.test.common" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" /> <!-- 개발 콘솔 -->
    </logger>
    <logger name="com.test.common.config" level="DEBUG" additivity="false">
        <appender-ref ref="INTERCEPTOR" /> <!-- 인터셉터 로그 -->
    </logger>
</configuration>
cs

 

logback 설정은 나중에 좀 더 자세히 알아보고,

nterceptor가 위치한 패키지의 경로를 41번 라인에서 지정해주고,

42번에서 지정한 appender-ref를 통해 위의 16번 라인의 appender 설정을 수행하게 된다.

 

가장 중요한 건, 41번 라인의 logger name 부분으로 여기에서는 로그를 찍을 대상 패키지를 지정해야한다.

 

이렇게 저장하면 큰 문제는 발생하지 않는데, 로컬에서는 내가 의도한대로 로그가 잘 남기는 하지만, 개발서버에서는 DispatcherServlet 로그가 잔존해서 남는 문제가 발생하고 있다. 문제까지는 아니긴 한데, 이게 왜 같은 패키지가 로컬/개발서버에서 각기 다르게 뜨는지는 조금 더 연구가 필요한 상황.

 

아무튼 이거 나중에 로그 파일 분리하는 방향으로 많이 활용될 것 같아 작성해보았다.

 

 

*** interceptor.log에 spring dispatcher servlet 로그 등이 남는 것들에 대해 수정하여 덧붙인다.

interceptor 로그에 위와 같이 작성할 경우, 스프링 프레임워크의 로그들도 남고 있었는데 오만가지 잡다한 설정을 수정해봐도 안되길래 혹시나, 싶은 마음으로 실행 스크립트를 살펴보니 --debug 옵션을 주고 있었다.

이걸 빼니까 위 설정만으로 해결이 완료됨ㅋㅋㅋㅋ

 

log 파일을 java interceptor 에서 직접 작성해야하나 아주 고민이 많았는데 다행히 잘 먹었다. ㅋㅋㅋ

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,
반응형

API 서버를 구현하다보면 뭐 Header에 토큰을 넣느니, 시스템 구분코드를 넣어서 타 시스템은 튕겨낸다느니 하는 일종의 Autorization 로직을 구현하는 경우가 있다. 내부망 시스템에서 토큰까지는 조금 오버인 것 같고, 다만 허용 시스템 코드를 리스트화 한 후에 허용된 시스템만 API를 태우자, 는 방침에 따라 대충 구현해봤다.

 

구글 제미니가 코드를 기깔나게 짜주었는데, 개발망에서 잘 안돌길래 두시간동안 쌩고생 하면서 고친 썰도 같이 푼다. 시작.

1. HeaderInterceptor.java 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class HeaderInterceptor extends HandlerInterceptorAdapter{
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) 
    throws Exception {
        String headAuth = req.getHeader("callSysCd"== null ? "" : req.getHeader("callSysCd");
        String authCode = "authCode";
        
        if(headAuth.equals(authCode)) {
            return true;
        }else {
            res.sendError(401"Not Authorized");
            return false;
        } 
    }
}
cs

 

HeaderInterceptor를 구현해준다. 대충 이렇게 구현하면 Spring 2.x 버전대에서는 HandlerInterceptorAdapter가 discard 되었다고 경고가 뜰 것이다. 그냥 써도 무관한데, 거슬리면 extends HandlerInterceptorAdapter 대신, implements HandlerInterceptor로 써주면 된다.

header에 callSysCd가 있는지를 판단하고, 있다면 "authCode"라는 문자열과 일치하는지 체크한다. 일치하면 그대로 API를 태우지만, 일치하지 않거나 없다면 401 에러를 return 하게 된다.

당연하게도, 검사문자열을 List 형태로도 구현할 수 있다.

 

2. WebMvcConfig.java 구현

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    HeaderInterceptor headerIntercepter;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(headerIntercepter).addPathPatterns("/test/**");
    }
}
cs

 

이렇게 구현해주고, 하단의 addPathPatterns에 걸러낼 url을 입력받는다. 위와 같이 입력받는 경우, localhost:8080/test/example.do와 같은 하위의 모든 url들은 interceptor에 의해 callSysCd의 여부를 체크할 것이다. 물론, localhost:8080/example.do 는 위의 패턴 url과 다르기에 여부체크를 하지 않는다.

 

이를 응용한다면, login.do는 header를 검사하지 않고 mypage.do와 같은 로그인이 필요한 부분에 대해서만 검사하는 식으로 응용이 가능하다.

 

3. 오류

외부망에서는 개발 순식간에 해두고, 개발망에서 적용했는데 interceptor가 하나도 안먹길래 대체 뭐가 문젠가 하고 뒤져보니 다음과 같이 별거 아닌곳에서 문제가 생겼었다.

나의 경우에는 위의 예시들이 com.test.config/* 디렉토리에 위치해 있었는데, 정작 메인메소드에서는 @SpringBootApplication(scanBasePackages="com.test.common") 이라고 되어있었으니 패키지 스캔이 제대로 안되어서 인터셉터들이 하나도 동작하지 않은것이었다. 로그도 안찍히고 해서 얼마나 당황했던지.

 

아무튼 별 거 아닌것을 찾아서 다행이었다.

반응형
블로그 이미지

김생선

세상의 모든것을 어장관리

,