반응형

1. 개요

 - Git webhook을 사용하여 배포 자동화 시스템을 구축해보자.


2. 준비

 - Jenkins

 - Git 레포지토리

 - 스프링 부트 / Gradle 프로젝트 (jar파일 배포)

 


3. webhook이란?

 - webhook이란 원격 저장소의 소스에 push, commit 등의 이벤트가 발생하면 Jenkins와 같은 CI 서버에 해당 이벤트를 전달하는 기능입니다. Jenkins에서 이 이벤트 정보를 받아 리빌드, 배포와 같은 작업을 연계하여 진행해보도록 하겠습니다.

 


4. Item 생성

 4.1. Freestyle project를 생성합니다. 

freestyle 프로젝트 생성

 

 4.2. 설명 및 GitHub project url을 입력합니다. 실제로 여기에 입력한 Git 주소가 연동에 사용되진 않습니다.

 

 4.3. 소스코드 관리로 Git을 체크한 후, 실제로 연동할 Git 주소를 입력합니다. 그럼 인증을 하라는 에러가 발생하며,  Git 계정 인증 시 'password 방식'이 지원을 하지 않으니 'Token authentication 방식'을 사용하라고 합니다. 그럼 토큰을 만들어야겠죠? 토큰은 깃허브 홈페이지에서 만들 수 있습니다.

Git 주소 연동

 

 4.4. 하던 작업을 잠깐 멈추고 '깃허브 로그인 / settings / developer settings / Personal access tokens' 탭에 들어가 Generate new token 버튼을 선택합니다.

token 생성

 

 4.5. 리포지토리에 대한 전체 접근 권한 및 hook 권한을 체크한 후, Generate Token 버튼을 눌러 토큰을 생성합니다.

  * repo, admin:repo_hook을 체크하면 됩니다.

  * Expiration을 No expiration으로 설정하면 토큰 만료 기간 없이 계속 사용 가능합니다.

token 권한 설정

 

 4.6. 생성된 토큰 값을 확인 후 복사합니다.

 * 참고로 해당 페이지를 넘어가게 되면 현재 발급된 토큰 키값을 다시 확인할 수 없으며, 분실 시 재발급 받아야합니다.

토큰 값 확인 및 복사

 

 4.7. Jenkins로 돌아가 Credentials / Add를 선택 후 Kind에는 Username with password, UserName에는 GitHub ID, Password에는 복사한 토큰 키를 입력합니다. 아래 ID와 Description은 식별자와 설명입니다. 임의로 입력 후 Add를 누릅니다. 에러가 사라졌다면 인증이 완료된 것입니다.

자격증명 등록

 

 4.8. Branch는 Master로 설정합니다.

 

 4.9. 빌드 유발은 GitHub hook trigger for GITScm polling을 선택합니다. 이 옵션이 있어야 webhook을 통해 Jenkins로 들어온 push 이벤트 정보를 인식하여 빌드를 유발할 수 있습니다. 실제 빌드는 다음 Build 탭에서 설정합니다.

빌드 유발

 

 4.10. 필자의 경우 Gradle SpringBoot 프로젝트이기때문에 Invoke Gradle Script를 선택 후 Tasks에 bootJar를 입력했습니다. build.gradle 파일에는 다음 코드를 입력하여 modu.jar라는 파일명으로 빌드되도록 설정한 상태입니다.

 * jar 파일 빌드가 완료되면 빌드 후 조치 부분에 빌드된 jar 파일을 실행시키도록 하여 스프링 부트에 내장되어 있는 tomcat 서버를 통해 서비스를 기동할 예정입니다.

build.gradle

 

Build 설정

 

 * Gradle Version에서는 빌드에 Gradle을 지정해줘야하는데 이는 Global Tool Configuration 탭에서 설정이 가능합니다.

 

 4.11. 빌드 성공 시 start.sh라는 스크립트를 실행하도록 합니다. 해당 스크립트안에는 현재 실행되고 있는 프로세스가 있다면 kill 후 재시작하는 코드가 작성되어 있습니다. Script에 해당하는 경로에 start.sh 파일을 적절히 커스터마이즈하여 생성해줍시다.

 

 * start.sh

#!/bin/bash

echo "PID Check..."
CURRENT_PID=$(ps -ef | grep java | grep modu | awk '{print $2}')

echo "Running PID: {$CURRENT_PID}"

if [ -z ${CURRENT_PID} ] ; then
        echo "Project is not running"
else
        echo "Kill Current PID"
        kill -9 $CURRENT_PID
        sleep 10
fi

echo "Deploy Project...."
nohup java -jar /var/lib/jenkins/workspace/practice-project/build/libs/modu.jar >> /home/ec2-user/practice-script/modu.log &

echo "Done"

~

 

 

 이걸로 간단한 형태의 Item 생성 및 설정이 끝났습니다. 이제 마스터 브랜치로 push를 하여 자동으로 빌드 및 jar파일을 실행시켜 배포가 되는지 확인해보도록 하겠습니다.


5. Git Push

 5.1. 소스코드 수정 후 GitHub Desktop 프로그램을 사용해 commit / push를 진행합니다.

push !


6. Jenkins 자동 빌드 및 배포 확인

 6.1. Push를 하면 자동으로 Jenkins 빌드가 시작됩니다. Build History에서 확인이 가능합니다.

자동 Build

 6.2. 해당 빌드 선택 후 Console Output을 확인하면 빌드 로그를 확인할 수 있습니다. 확인 결과, 빌드에 성공하여 BUILD SUCCESSFUL를 뿌리고 있습니다. 그에 따라 start.sh 스크립트를 실행하는 로그도 확인되고 있습니다. 스크립트도 정상적으로 실행되었으니 실제로 서비스가 올라갔는지 확인해보도록 하겠습니다.

 

Build Log

 

 6.3. public DNS로 조회하여 서비스 확인 결과, 정상적으로 jar파일이 실행되어 tomcat 서비스가 올라간 것을 확인할 수 있습니다.

서비스 정상 기동 확인


7. 마치며...

 Jenkins를 사용하여 정말 간단한 구조의 DevOps 환경을 구성해보았습니다. SpringBoot / Gradle 프로젝트, Git을 통한 소스관리, Jenkins를 통한 자동 빌드 및 배포, Swagger를 통한 REST API Interface를 제공하게 되었네요.. 뿌듯..

 아무래도 프리티어 서버를 통해 작업을 진행하다 보니 중간중간 메모리 부족으로 인한 문제도 발생하였습니다. 간혹가다 빌드 시 OutofMemory가 발생하거나, 서버 자체가 꺼져버리는 현상이었습니다. 그럴때마다 AWS EC2 인스턴스를 재시작했었는데, 현재는 Swap Memory를 0 > 2GB로 늘려주어 해결한 상태입니다. 관련 포스팅 다음에 진행하여 링크를 남겨놓도록 하겠습니다!

반응형
반응형

1. 개요

 - Amazon Linux 2 OS에 Jenkins 설치


2. 준비

 - Amazon Linux 2 서버


3. 데몬화 모듈 설치

 - Jenkins를 설치하는 방법 중 하나는 yum 명령어를 이용하는 것이다. 그런데 Amazon Linux 2 서버에서는 yum 명령어를 사용하여 설치했을 때 daemonize 에러가 발생한다.

Package: jenkins-2.308-1.1.noarch (Jenkins) Requires: daemonize

 

 - Amazon Linux 2 OS는 다른 linux os와는 다르게 daemonize 라는 모듈을 기본으로 지원하지 않는다. 때문에 daemonize를 install 해줘야한다. 다음 명령어를 통해 설치를 진행하자

vim /etc/yum.repos.d/epelfordaemonize.repo // epelfordaemonize.repo 파일 생성

-- 다음 코드 입력 후 저장 --
[daemonize]
baseurl=https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/
gpgcheck=no
enabled=yes

 

 - 만약 저장 시 readonly 에러가 뜰 경우 :wq가 아닌 다음 명령어를 입력하자

:w !sudo tee % > /dev/null

 

 - 저장이 완료되면 모듈을 설치한다.

yum install daemonize -y //daemonize 설치

4. Jenkins 설치

 - 데몬화 모듈 설치가 완료되면 Jenkins를 설치한다.

 - 이왕이면 현재 서버의 Java 버전과 동일한 버전으로 Jenkins를 설치한다.

yum install jenkins java-1.8.0-openjdk-devel -y

 


5. Jenkins 실행

 - 설치가 완료되면 Jenkins를 실행한다. 만약 권한 에러가 뜨면 sudo를 붙여주자

sudo service jenkins start  //jenkins start
sudo service jenkins stop   //jenkins stop

 

 - 시작 명령어를 입력했을 때 다음과 같은 로그가 출력될 경우 정상적으로 서비스가 올라간 것이다.

jenkins start

 - 프로세스 확인 명령어로 실행 여부를 확인 가능하다. port, pid 등의 정보가 조회된다.

ps -ef | grep jenkins

프로세스 정보

 

 - 이제 실제 Jenkins 페이지로 접속해보자. 기본적으로 ip:8080 포트를 입력하면 접속된다.

시작화면

  - 만약 8080 포트가 이미 사용중이라면 포트를 변경해주어야한다. 필자의 경우 9090으로 변경 후 AWS 보안그룹에 내 IP 에 대해 9090 포트를 오픈해주었다. 포트 변경 명령어는 다음과 같다.

sudo vim /etc/sysconfig/jenkins //jenkins 설정파일

코드에서 JENKINS_PORT 값을 원하는 포트로 수정 후 저장

 

  -  수정한 포트는 Jenkins 재시작 시 적용된다.

 


6. 마치며

 - 다음은 Jenkins와 git, gradle을 연동하여 git 마스터 브랜치에 push가 갈 경우 빌드 및 배포 자동화에 대한 블로깅을 하도록 하겠다. 실제로 작업은 해놓았으나, 내용 전달을 위한 정리가 아직 되지않았다.

반응형
반응형

1. 개요

  • AWS 서버에 스프링 부트 프로젝트를 jar파일 배포한다.

2. 준비

  • Amazon Linux 2 서버
  • Spring Boot + gradle 프로젝트
  • JDK 설치
  • Git 설치 및 Source Clone

3. JDK 설치

  • 프로젝트를 배포하기 전 AWS 서버에 JDK를 설치해야합니다. SpringBoot의 build.gradle 파일의 sourceCompatibility를 확인해봅시다. sourceCompatibility는 현재 프로젝트에 호환된 Java 버전을 의미합니다.

프로젝트 java 버전 확인

 

  • 필자의 경우 java 1.8 이므로 aws서버에 openJDK 1.8 버전을 설치를 위해 다음 명령어를 입력하겠습니다.
 sudo yum install -y java-1.8.0-openjdk-devel.x86_64

 

  • 설치가 완료되었다면 인스턴스의 Java 버전을 8로 변경해야합니다. 다음 명령어를 입력 후 인스턴스에 적용할 java 버전의 번호를 입력합니다.
sudo /usr/sbin/alternatives --config java

자바 버전 선택

  • 필자의 경우 방금 설치한 java 버전만 있고, Selelction이 1로 지정되어 있으므로 입력창에 1을 입력합니다.
  • 설정이 완료되면 서버의 java 버전을 확인합니다.
java -version

자바 버전 확인

  • 설정한 Java 버전이 조회된다면 스프링 부트 jar 배포에 대한 기본적인 준비는 끝났습니다.이제 배포를 해봅시다!

4. Git 설치 및 Clone

  • 프로젝트 파일이나 빌드된 파일을 AWS 서버로 전송 해야합니다. 이 과정에 대해 채택할 수 있는 방법은 FTP가 될수도 있지만 필자의 경우 Git Clone을 통해 프로젝트 파일을 AWS 서버에서 받은 후 서버 내에서 빌드를 하는 방식을 채택했습니다.
  • 다음 명령어를 입력해 AWS 서버에 git을 설치 및 확인합니다.
sudo yum install git // git 설치
git --version //git 설치(버전) 확인

git 설치버전 확인

  • 설치가 완료되면 clone 될 디렉토리를 생성한다. 필자의 경우 홈 디렉토리 기준으로 "app" 폴더를 생성하여 그 안에 git에서 clone한 소스를 넣겠습니다.
mkdir ~/app  //app 폴더생성

 

  • 폴더 생성이 완료되면 본인의 깃허브 웹페이지에서 주소를 복사합니다.

Clone할 url 복사

  • AWS 서버에 다음 명령을 입력하여 clone을 진행합니다.
git clone 복사한 주소

 

  • clone이 정상적으로 완료되면 app 폴더 안에 프로젝트 소스들이 들어갔을겁니다. 이제 배포를 진행할 차례입니다.

5. 배포

  • 프로젝트 경로로 들어가 다음 명령어를 입력해 jar 빌드합니다.
./gradlew bootjar

 

  • build가 완료되면 자동적으로 build/libs 폴더 내에 "프로젝트 명/버전.jar" 형식으로 파일이 생성됩니다. 배포는 간단합니다. 이 파일을 실행시켜주면 됩니다. 스프링 부트는 jar 파일 빌드 시 tomcat 서버가 내포된 형태로 빌드되기 때문에 별도의 tomcat 서버가 필요없습니다.
java -jar jar파일 명

jar 파일 실행

  • 배포가 완료되었습니다. 배포된 서버로 접속하기 위해 '퍼블릭 DNS:포트'를 입력해봅시다.
  • 퍼블릭 DNS는 AWS 인스턴스 상세정보로 들어가면 확인할 수 있으며 포트는 배포 로그의 마지막 부분에서 확인할 수 있습니다.

퍼블릭 DNS

 

  • 짜잔~ 다음과 같이 정상적으로 접속이 되었습니다.

배포된 어플리케이션 접속 성공 화면

 

  • 만약 접속이 되지 않으신다면 AWS내에 설정한 보안 그룹에 해당 포트가 오픈되어있는지 확인해봅시다. 필자의 경우 해당 포트가 열려있지 않아 최초에 접속이 되지 않았습니다.
  • 인스턴스 세부정보 > 보안 > 인바운드 규칙에 8090포트를 추가해줍시다.
    인바운드 규칙 추가

6. 마치며...

  • Git 연동 및 클론, 빌드, AWS 배포를 해보았습니다. 되게 간소하게(?) 진행했지만 AWS 서버에 배포를 해본건 처음이라 감회가 새로웠다. 다음 목표는 Jenkins를 연동하여 git commit이 있을 때 자동으로 AWS 서버에 수정된 코드가 배포되도록 하는 것이다. 이 작업이 끝나면 본격적인 서버 로직을 작업할것이다
반응형
반응형

1. 개요

 - 프리티어 계정으로 EC2 서버를 생성한다.

 - 생성된 EC2 서버에 접속한다.


2. 환경

 - Window 10

 - AWS 계정 생성 완료


3. EC2 란?

 - Elastic Compute Cloud의 약자로 AWS에서 제공하는 사양, 용량 등을 유동적으로 사용 가능한 서버를 말합니다.

 - 프리티어 계정은 이 중 t2.micro(CPU 1, 메모리 1GB, 용량 최대 30GB)라는 서버를 1년동안 무료로 사용 가능합니다. 

 - 프리티어 기준으로 월 750 시간의 제한이 있으며 초과하면 과금이 발생합니다. 하지만 24*31은 744 시간이므로 한대의 프리티어 서버를 운용한다면 과금은 발생하지 않습니다.


4. EC2 t2.micro 서버 생성

 4.1. 검색창에 ec2 검색 및 선택

ec2 검색

 

 4.2. 인스턴스 시작 버튼 선택

  - 화면 진입 시 상단에 위치한 인스턴스 시작 버튼을 선택합니다.

인스턴스 시작

 

 4.3. OS 선택

  - 프리티어로 사용 가능한 Amazon Linux 2 AMI 64비트를 선택합니다.

 

 4.4. EC2 유형 선택

  - 프리티어로 사용 가능한 t2.micro 를 선택합니다.

 

 4.5. 인스턴스 세부 정보 구성

  - 이 부분에서 딱히 설정해야할 부분은 없습니다. 넘어갑시다.

 

 4.6. 스토리지 설정

  - t2.micro 에서 지원 가능한 최대 스토리지 용량인 30GB를 입력합니다.

 

 4.7. 태그 추가

  - Name 이라는 Key를 생성하고 값(ex. 프로젝트 명)을 입력해줍니다.

 

 4.8. 보안 그룹 추가

 - [SSH, 내 IP] : SSH를 통해 서버에 접근할 수 있도록 추가합니다. '나'만 접근 가능하도록 하기 위해 '내 IP'로 설정하였습니다. 만약, 서버에 접근해야할 동료가 있다면 SSH 유형을 하나 더 만들어 추가해줍시다.

 - [사용자 지정 TCP, 8080] : 8080 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 - [HTTPS, 443] : 443 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 

 4.9. 검토

  - 다음 항목을 검토 후 이상이 없다면 시작버튼을 클릭합니다.

   - OS : Amazon Linux 2

   - 인스턴스 유형 : t2.micro

   - 보안그룹

   - 스토리지 : 30GB

 

 4.10 키페어 설정

 - 키 페어 설정을 하게 되면 서버 접근 시 접근자의 개인키와 서버의 공개키를 체크합니다. 보안그룹을 설정했지만 키페어 설정도 꼭 필요합니다.

  - 현재 보안 그룹 설정을 했기 때문에 마스터 서버로의 접근은 '내 IP'에서만 가능합니다. 악의적인 사용자가 '나'의 서버 접근 정보를 알고있다고 하더라도 IP가 달라 접근이 불가능합니다. 하지만 극단적인 예로, 내 옆집에 사는 사람이라면 제 공인 IP로 접근이 가능할 수 있고, 그렇게 되면 제 서버로의 접근도 가능할 수 있습니다. 만약, 사내 프로젝트를 위해 EC2 서버를 생성하고 보안그룹을 '내 IP'로 설정하였습니다. 그렇게되면 같은 사내망을 사용하는 모든 사람들이 접근 가능하게 됩니다.

  - 이런 유형의 접근을 막기 위해 필요한 것이 바로 키페어입니다. 키페어를 설정하면 서버의 공개키와 대응되는 개인키를 발급받게 되고, 실제 접근 시 이 키파일을 서버로 제공해야합니다.

 - RSA 유형으로 키 페어 생성 선택 후 키 페어 이름에 프로젝트 명을 넣습니다. 그 후 키 페어 다운로드를 클릭합니다.

 - pem 확장자의 개인키 파일 다운로드가 확인되면 인스턴스 시작 버튼을 클릭합니다.

 

 4.11. EC2 인스턴스 생성 완료

  - 다음과 같은 화면이 나올 경우 EC2 인스턴스가 생성 완료된것입니다.

  - 이제 개인키 파일을 통해 내가 만든 EC2 서버에 접속해볼 차례입니다.


5. EC2 서버 접속

 - 서버에 접근하기 위해서는 몇가지 과정이 필요합니다. 차근차근 밟아봅시다.

 

 5.1. putty, puttygen 설치

  - putty : SSH를 통한 서버 접속에 사용합니다.

    * 설치 url : https://putty.softonic.kr/

 

PuTTY

완전하고 안정적인 텔넷 및 SSH 클라이언트

putty.softonic.kr

  - puttygen : 서버로 접근하기 위해서는 개인키 파일(.pem)을 .ppk 확장자로 변환해야 하며, 이 변환에 사용합니다. 

    * 설치 url : https://www.puttygen.com/download-putty

 

 5.2. puttygen 실행

  - puttygen.exe 실행 후 Conversions > import key 를 선택합니다.

import key

 5.3. 개인키 파일 임포트

  - 아까 발급받은 개인키 파일을 선택합니다.

pem 파일 선택

 

 5.4. ppk 파일 생성

  - 다음과 같은 화면이 조회되면 Save private Key 버튼을 선택하여 ppk 파일을 생성합니다.

  - 알림 메시지가 하나 뜨는데 무시하고 예를 누릅니다.

  - 파일이 생성되었다면 이제 서버로 접속할 차례입니다.

ppk 파일 생성

 5.5. putty 실행

  - putty를 실행한 후 HostName에 ec2-user@[퍼블릭 DNS]를 입력합니다.

  - ec2-user는 Amazon Linux 2 생성 시 자동 생성되는 ID 입니다. 다른 OS를 선택했다면 헤매지 말고 구글링!

HostName 입력

  

  - 만약, 퍼블릭 DNS를 모르겠다면 EC2 콘솔에 접속해 인스턴스 상세보기로 확인할 수 있습니다.

 

 5.6. Auth 설정

  - putty 카테고리의 Connection > SSH > Auth를 선택 후 Browse 버튼을 선택하여 생성한 ppk 파일을 로드합니다.

Auth 설정

 5.7. Open!

  - 모든 설정이 끝났습니다. 이제 Open 버튼을 눌러 서버에 접속해봅시다!

  - 다음과 같은 화면이 뜬다면 성공입니다!

  - 만약 에러가 뜨신다면 다음을 확인해보세요.

    1) 서버에 대한 개인키가 맞는지

    2) OS에 대한 ID (ex. amazon linux = ec2-user)가 맞는지

    3) HOST NAME에 퍼블릭 DNS가 정확하게 입력됐는지

  - 저의 경우 2번과 3번이 잘못되어 시간이 걸렸답니다 ㅠㅠ.


6. 마치며

 - 예전에 책을 보며 무작정 따라한 적이 있는데, 이게 무슨 설정이고, 왜 하는지도 이해가 가지 않았었던것 같아요. 용어의 의미와 역할을 이해하려고 노력하며 다시 책을 따라해보세요! 훨씬 의미있고 기억에 남을거에요!

반응형
반응형

1. 개요

 - 스프링 부트 환경에서 기본으로 제공하는 LogBack을 사용하여 로그를 남겨보자

 - spring.profiles.active를 사용하여 운영, 개발 환경에 따라 로그 설정을 분기하여 적용해보자

 [참고 : https://goddaehee.tistory.com/206 깃대희의 작은공간]

 

2. LogBack이란?

 - LogBack이란 Log4j를 만든 개발자가 Log4j를 기반으로 속도와 메모리 점유율을 개선하여 만든 로깅 프레임워크이다.

 - org.slf4j.Logger 인터페이스의 구현체이다.

   >> 코드 작성 시 이 인터페이스를 임포트해주면 된다.

 

3. LogBack 특징

 - Level : 로그 레벨을 설정할 수 있다.

 - Appender : 출력 방법을 선택할 수 있다. ex) Console, RollingFile 등

 - Logger : 로그마다 다른 설정을 적용시킬 수 있다.

 - Authmatic Reloading Configuration File : 특정 시간마다 별도의 스레드를 통해 설정 파일 변경 여부 파악 및 적용이 가능하다. > logback 설정 변경 시 서버 재시작이 필요없다.

 

4. 프로젝트 세팅

 1) Controller - 로그를 찍기 위한 클래스

 2) application.properties - spring.profiles.active 설정 추가

 3) logback-spring.xml - logback 설정

 4) logback-{운용환경}.properties - logback 설정파일에서 로드할 상수 (로그파일, 레벨 등)

 

 4.1. Controller

  /api/log 요청 시 trace부터 error 까지의 로그를 쌓는 메서드를 작성한다.

@Controller
@RequestMapping("/api")
public class FileController {

	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@GetMapping("/log")
	@ResponseBody
	public ResponseDTO main() {
		logger.trace("trace Log");
		logger.debug("debug Log");
		logger.info("info Log");
		logger.warn("warn Log");
		logger.error("error Log");
		
		return null;
	}
}

 

 4.2. application.properties

  spring.profiles.active=dev 를 추가하여 개발 및 운영 환경을 정의해준다.

#서버포트
server.port=9090

#운용환경 : 개발
spring.profiles.active=dev

 

 4.3. logback-spring.xml

resource 경로에 logback-spring.xml 을 생성한 후 logback 설정을 정의한다.

* resource 경로에 logback-spring.xml 파일이 있으면 서버 기동 시 자동으로 로드한다.

<?xml version="1.0" encoding="UTF-8"?>

<!-- 10초마다 파일 변화를 체크하여 갱신시킨다. -->
<configuration scan="true" scanPeriod="10 seconds">

	<!-- spring.profile에 따른 설정파일 분기 -->
	<springProfile name = "prod">
		<property resource = "logback-prod.properties"/>
	</springProfile>
	
	<springProfile name = "dev">
		<property resource = "logback-dev.properties"/>
	</springProfile>
	
	
	<!-- 루트 로그 레벨 -->
	<property name ="LOG_LEVEL" value = "${log.config.level}"/>
	
	<!-- 로그 파일 경로 -->
	<property name ="LOG_PATH" value = "${log.config.path}"/>
	
	<!-- 로그 파일 명 -->
	<property name ="LOG_FILE_NAME" value = "${log.config.filename}"/>
	<property name ="ERR_LOG_FILE_NAME" value = "${log.config.filename}_error"/>
	
	<!-- 로그 파일 패턴 -->
	<property name ="LOG_PATTERN" value = "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] [%logger{0}:%line] - %msg%n"/>
	
	
	
	<!-- 콘솔 Appender 설정 -->
	<appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender">
		<encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
	</appender>
	
	<!-- 파일 Appender 설정 -->
	<appender name="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 파일 경로 설정 -->
		<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
		
		<!-- 로그 패턴 설정 -->
		<encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
		
		<!-- 롤링 정책 -->
		<rollingPolicy class = "ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- gz, zip 등을 넣을 경우 자동 로그파일 압축 -->
			<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${LOG_FILE_NAME}_%i.log</fileNamePattern>
			
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- 파일당 최고 용량 -->
				<maxFileSize>10MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
			
			<!-- 로그파일 최대 보관주기 -->
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
	
	
	<appender name = "ERROR" class ="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class ="ch.qos.logback.classic.filter.LevelFilter">
			<level>error</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
		<file>${LOG_PATH}/${ERR_LOG_FILE_NAME}.log</file>
		
		<!-- 로그 패턴 설정 -->
		<encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
		
		<!-- 롤링 정책 -->
		<rollingPolicy class = "ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- gz, zip 등을 넣을 경우 자동 로그파일 압축 -->
			<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${ERR_LOG_FILE_NAME}_%i.log</fileNamePattern>
			
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- 파일당 최고 용량 -->
				<maxFileSize>10MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
			
			<!-- 로그파일 최대 보관주기 -->
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
	
	<root level = "${LOG_LEVEL}">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</root>
	
	<logger name="org.apache.ibatis" level = "DEBUG" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>
</configuration>

 - springProfile 태그를 사용하여 시스템 변수의 spring.profile.active 값을 조회할 수 있다. 이 값에 따라 logback-prod, logback-dev 파일 중 하나를 로드하여 설정파일의 프로퍼티로 사용한다.

 

 4.4. logback-dev.properties

 - 개발환경에서는 로그레벨을 debug로 설정한다.

#로그 레벨
log.config.level=debug

#로그파일 경로
log.config.path=/logs

#로그파일 명
log.config.filename=reckey

 

 4.5. logback-prod.properties

 - 운영 환경에서는 로그레벨을 info로 설정한다.

#로그 레벨
log.config.level=info

#로그파일 경로
log.config.path=/logs

#로그파일 명
log.config.filename=reckey

 

5. 테스트

 spring.profile.active를 prod 설정하면 로그레벨이 INFO로 잡혀있어 불필요한 로그는 조회되지 않지만 dev로 설정할 경우 서버 기동 시 발생되는 로그, DB 로그 등이 중구난방으로 나오게 된다.

 원인은 dev로 설정할 경우 logback 설정의 루트 로그 레벨이(root level) 값이 debug로 설정되어 어플리케이션 내 모든 클래스의 debug 로그가 찍히기 때문이다.

 이런 로그들은 특수한 목적이 없는 한 debug보단 info로 올리는 것이 좋다. 이를 위해서는 logger 태그를 사용하여 클래스별 로그 레벨 분기처리를 해야한다.

DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [AnonymousAuthenticationFilter:96] - Set SecurityContextHolder to anonymous SecurityContext
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FilterSecurityInterceptor:210] - Authorized filter invocation [GET /api/log] with attributes [permitAll]
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FilterChainProxy:323] - Secured GET /api/log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [DispatcherServlet:91] - GET "/api/log", parameters={}
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [PropertySourcedRequestMappingHandlerMapping:108] - looking up handler for path: /api/log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestMappingHandlerMapping:522] - Mapped to com.reckey.controller.FileController#main()
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:24] - debug Log
INFO  2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:25] - info Log
WARN  2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:26] - warn Log
ERROR 2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:27] - error Log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestResponseBodyMethodProcessor:268] - Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [application/json, application/*+json, application/json, application/*+json]
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestResponseBodyMethodProcessor:298] - Nothing to write: null body
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [DispatcherServlet:1131] - Completed 200 OK
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [HttpSessionSecurityContextRepository:346] - Did not store anonymous SecurityContext
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [SecurityContextPersistenceFilter:118] - Cleared SecurityContextHolder to complete request
DEBUG 2021-10-07 02:11:25[reckeyPool housekeeper] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:11:25[reckeyPool housekeeper] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.

 

 5.1. 특정 클래스 분기처리 (로그레벨 INFO로 격상)

  일단 분기처리할 클래스의 경로를 확인해보자. 현재 로그 설정으로는 클래스 명만 나오고 있기 때문에 클래스 경로를 파악하기 힘들다. 로그 패턴에 클래스 경로 패턴인 %C를 다음과 같이 추가해보자.  자동 리로드 설정이 있으므로 서버 재시작은 하지 않아도 된다.

<property name ="LOG_PATTERN" value = "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] [%C] [%logger{0}:%line] - %msg%n"/>

 그리고 다시 요청을 보내면 다음과 같이 클래스 경로가 함께 조회된다.

INFO  2021-10-07 02:14:37[restartedMain] [org.springframework.boot.StartupInfoLogger] [ReckeyApplication:61] - Started ReckeyApplication in 0.703 seconds (JVM running for 3806.024)
DEBUG 2021-10-07 02:14:37[restartedMain] [org.springframework.boot.availability.ApplicationAvailabilityBean] [ApplicationAvailabilityBean:77] - Application availability state LivenessState changed to CORRECT
INFO  2021-10-07 02:14:37[restartedMain] [org.springframework.boot.devtools.autoconfigure.ConditionEvaluationDeltaLoggingListener] [ConditionEvaluationDeltaLoggingListener:63] - Condition evaluation unchanged
DEBUG 2021-10-07 02:14:37[restartedMain] [org.springframework.boot.availability.ApplicationAvailabilityBean] [ApplicationAvailabilityBean:77] - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@36e9f17b
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@73eb69c2
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@439a7476
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@42521e57
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@3bc95d4f
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@13c4968d
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@36d6947
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - After adding stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:15:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:16:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:16:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:16:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:16:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:17:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:17:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.

 org.springframework경로의 여러 클래스와 com.zaxxer.hikari로 시작하는 여러 클래스에서 DEBUG레벨로 로그들이 찍히고 있다. (히카리 로그는 DB연결로 인함입니다. 신경쓰지않으셔도됩니다.) 이 클래스들만 DEBUG레벨에서 INFO 레벨로 올린다면 이 문제가 해결될 것이다.

logback-spring.xml 파일의 logger 태그에 분기처리할 클래스를 다음과 같이 추가한다.

name에 들어가는 경로의 하위 모든 클래스에서 발생하는 로그를 INFO 레벨로 올리게 된다.

	<logger name="org.springframework" level = "INFO" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>
	
	<logger name="com.zaxxer.hikari" level = "INFO" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>

 

이제 실제 요청을 보내면 다음과 같이 불필요한 debug 레벨의 로그들을 쌓지 않게 된다.

DEBUG 2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:24] - debug Log
INFO  2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:25] - info Log
WARN  2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:26] - warn Log
ERROR 2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:27] - error Log

 

6. 마치며

 스프링 부트에서 로그백을 사용하여 로그를 남겨본 것은 처음이다. 생각보다 되게 간단하고, log4j 같은 경우 로그파일 리로드 설정을 외부 설정파일에서 해줘야하는데 logback은 리로드되는 기능이 설정 파일 안에 있어서 좋은것 같다.

반응형
반응형

1. 개요

 이전 포스팅에서 테스트 코드를 통해 두 클래스를 비교한 결과 다음과 같은 정보를 얻을 수 있었다.

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 메서드 존재 여부 X O
HTTP 통신 횟수 1 ConnectionPool 설정에 따라 다름
ConnectionPool X O

오늘 알아볼 것은

첫째, 생성한 DefaultHttpClient에서 execute를 2번 이상 실행했을 때 즉, HTTP 통신을 2번 이상 요청했을 때 예외가 발생한 원인과 생명주기.

둘째, CloseableHttpClient의 ConnectionPool 설정 정보를 jar 파일을 통해 알아보겠다.


2. DefaultHttpClient

jar파일과 예외 로그를 기반으로 코드를 찾아가보니 다음과 같은 부분에서 예외가 발생함을 확인하였다.

execute 메서드를 따라가다.

conn이라는 값이 null이 아닐 때 위 에러가 발생한다. conn은 ManagedClientConnectionImpl 형의 멤버필드였으며, 어디선가 주입이 된것같은데... 결론은 찾을 수가 없었다. 멍청한자식.

 

서치를 통해 얻은 정보를 정리한 결과 예외 발생 원인은 다음과 같았다.

DefaultHttpClient 객체를 생성하면 내부적으로 basicClientConnectionManager 인스턴스가 주입된다. 이 인스턴스는  HTTP 통신에 대한 커넥션 정보를 저장하고 있다. 단, 하나의 최초 연결한 하나의 커넥션 정보만 저장한다.

두 개의 커넥션을 연결하려 했기때문에 예외가 발생했으며, 실제 에러 로그를 확인해보니 basicClientConnectionManager 클래스의 메서드 안에서 발생함을 확인할 수 있었다.

아래 로그의 Asserts.java:34가 위 코드 사진의 첫번째 빨간 블럭부분이었다. (기존 커넥션 정보가 있기때문에 발생)

 

정리하면 DefaultHttpClient 클래스는 하나의 HTTP 통신만을 처리할 수 있도록 내부적으로 구현되어져 있다.

만약 이 인스턴스를 사용해 두번의 통신을 처리하고싶다면 두개의 인스턴스를 생성해야한다.


3. CloseableHttpClient

생성자를 찾아가보니 다음과 같은 코드가 있었다. httpClientBuilder.create().build(). 요녀석을 파헤쳐보자

createDefault()

create().build() 메서드 확인 결과, PoolingHttpClientConnectionManager를 생성한 후 connectionManager로 사용하는 것이 보인다.

create().build()

 

생성된 ConnectionPool의 Default maxTotal, maxConPerRoute는 다음과 같이 2, 20임을 확인할 수 있다.

CPool

변수
maxTotal 최대 커넥션 개수
maxConPerRoute 라우트당 최대 커넥션 개수(ip:port 별 최대 커넥션 개수)

 

커스텀을 하지 않고 사용할 경우 최대 커넥션 개수가 2개이기때문에 실제 서비스를 운영하기엔 문제가 있다.

PollingHttpClientConnectionManager 은 커스텀이 가능하다. 때문에 상황에 맞게 커스텀하여 CloseableHttpClient를 구현한다면 많은 HTTP 통신 요청을 필요로 하는 서비스에 적절하게 사용될 수 있다.


4. 마치며

DefaultHttpClient와 CloseableHttpClient에 대한 아~주 미세한 차이에 대해서도 몰랐었지만, 이번 스터디를 통해 차이점은 물론이며, 실제 서비스에 왜 저 클래스를 사용했는지도 이해하게 되었다.

DefaultHttpClient의 생명주기, 언제 connection이 끊어지는지에 대해서는 확인하지 못해 뭔가 깨림직한 기분이지만, 오늘하루도 잘 보냈음에 위안을 삼는다!

반응형
반응형

1. 개요

 예전에 서비스 내에서 HTTP을 사용해 HR 시스템에서 정보를 가져오는 로직에 문제가 발생한 적이 있었다.

 원인은 사용하는 HttpClient객체가 static으로 선언되어 있어 멀티 쓰레드 환경에서 통신이 꼬여버린 것이다.

 해결방안으로 HttpClientBuilder를 사용해 PoolingHttpClientconnectionManager, requestConfig 객체를 주입받은   CloseableHttpClient 객체를 싱글톤으로 등록 후 호출할때마다 재사용하는 방식을 사용했다.

 

 그런데 다른 프로젝트의 SM업무를 맡던 도중 HTTP 통신할때마다 DefaultHttpClient 객체를 생성하고 있었다. 또한 인스턴스를 통신할때마다 생성하는 부분은 있지만 close시키는 부분이 없어서 DefaultHttpClient가 어떤녀석인지, 그리고 CloseableHttpClient와 무슨 차이점이 있는지 궁금해졌다.


2. CloseableHttpClient와 DefaultHttpClient

  이 두 클래스 모두 HttpClient 인터페이스의 구현클래스이다. 하지만 이녀석들의 차이에 대해 상세하게 정리된 내용을 찾지못해 실제 테스트를 통해 알아보기로 했다.


2.1. DefaultHttpClient, CloseableHttpClient 테스트코드 1.

 각각의 클래스에 DefaultHttpClient, CloseableHttpClient 인스턴스 생성 후 HTTP GET 통신을 하는 코드를 작성하였다.


2.2. CloseableHttpClient

DefaultHttpClient를 사용하여 HTTP GET 통신을 하는 코드이다.

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
public class UseHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        HttpClient httpClient = null;
        try {
            httpClient = new DefaultHttpClient(); // httpClient 4.3버전 이후 deprecated 처리.
            
            HttpGet httpGet = new HttpGet(URL);
            
            HttpResponse response = httpClient.execute(httpGet);
            
            System.out.println(":: DefaultHttpClient Response ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
 
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
cs

17번째 라인부터는 응답 데이터를 읽어오는 부분인데 네이버 홈페이지 코드가 방대하게 나와서 굳이 출력하진 않았다.

중요한건 DefaultHttpClient 인스턴스를 사용해 HTTP 통신을 한번 했다는 점이다.


2.2. CloseableHttpClient

 CloseableHttpClient를 사용하여 HTTP GET 통신을 하는 코드이다.

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
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

마찬가지로 16번째 라인부터는 응답 데이터를 읽어오는 부분이다.

여기서도 중요한건 CloseableHttpClient 인스턴스를 사용해 한번 통신했다는 점이다.


2.3. 첫번째 테스트로 인해 알게된 차이점

 첫번째 테스트로 인해 알게된 이 두 클래스의 차이점을 정리해보았다. 생성방식이 달랐고, CloseableHttpClient 클래스는 close 메서드가 있었다. 왜 있지?? 아직까지는 큰 차이를 느끼진 못해 다음 테스트를 진행하였다.

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 여부 X O

3.1. DefaultHttpClient, CloseableHttpClient 테스트코드 2.

 close라는 부분이 눈에 밟혀 각각의 인스턴스에서 HTTP 통신 메서드인 execute를 두번씩 호출해보았다.


3.2. DefaultHttpClient

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
public class UseHttpClient {
 
    private static final String URL = "http://www.naver.com";
    
    public static void main(String[] args) {
        HttpClient httpClient = null;
        try {
            httpClient = new DefaultHttpClient(); // httpClient 4.3버전 이후 deprecated 처리.
            
            HttpGet httpGet = new HttpGet(URL);
            
            HttpResponse response = httpClient.execute(httpGet);
            HttpResponse response2 = httpClient.execute(httpGet); // 추가한 코드
            
            System.out.println(":: DefaultHttpClient Response ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode()); // 추가한 코드
 
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
cs

13번 라인과 17번 라인에 호출 및 응답 코드를 출력하는 코드를 추가하였다.

실행 결과 다음과 같은 Exception이 발생하였다.

13번 라인에서 Exception 발생

예외 메시지는 "연결이 여전히 할당되어 있습니다. 다른 연결을 할당하기 전에 연결을 해제해야 합니다." 라는 뜻이다.

12번 라인에서 수행된 연결이 아직 끊기지 않은 상태에서 13번 라인의 execute 코드가 실행되어 그런 것같다. 그렇다면 과연 언제 끊기는 걸까? 생명주기가 궁금해졌지만 이는 더 파고들어야 알수있을 것 같다. 일단 여기서 확인된 점은 DefaultHttpClient 클래스는 생명주기가 끝나기 전까지 한번의 HTTP 요청을 수행할 수 있다는 것이다.


3.3. CloseableHttpClient

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
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            CloseableHttpResponse response2 = httpClient.execute(httpGet); // 추가한 코드
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode()); // 추가한 코드
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

마찬가지로 11, 15번 라인에 통신 및 응답코드를 출력하는 코드를 추가하였다.

실행 결과, 예외가 발생하지 않으며, 두 요청 모두 response Code 200을 응답받았다.

그렇다면 이 CloseableHttpClient의 생명주기는 어떻게 될지가 궁금해졌다. 마침 close 메서드도 있으니 추가 테스트를 진행해보았다. 그런데 예상치 못한 예외 코드가 출력되었다.


3.4. CloseableHttpClient close()

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
public class UseCloseableHttpClient {
 
    private static final String URL = "http://www.naver.com";
    public static void main(String[] args) {
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpGet httpGet = new HttpGet(URL);
            
            CloseableHttpResponse response = httpClient.execute(httpGet);
            httpClient.close(); // 추가한 코드
            CloseableHttpResponse response2 = httpClient.execute(httpGet);
            
            System.out.println(":: CloseableHttpResponse ::");
            System.out.println(":: response 1 result code : "+response.getStatusLine().getStatusCode());
            System.out.println(":: response 2 result code : "+response2.getStatusLine().getStatusCode());
            
            BufferedReader reader= new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String inputLine;
            StringBuffer responseBuf = new StringBuffer();
            
            while((inputLine = reader.readLine()) != null) {
                responseBuf.append(inputLine);
            }
 
            reader.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if(httpClient != null) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}
cs

 

11번 라인에서 Exception 발생

커넥션 풀이 종료됐다는 예외였다!

아. 생성한 CloseableHttpClient 인스턴스는 기본적으로 커넥션 풀을 지원한다는 것을 알게되었고, close 메서드는 이 인스턴스에 할당된 커넥션 풀을 close 시키는 것이었다.

3.2 테스트에서 2번을 연속으로 호출했을 때 예외가 발생하지 않았던 이유도 요청을 커넥션 풀을 통해 요청이 처리되기 때문이었다.


3.4. 두번째 테스트로 인해 알게된 차이점

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 여부 X O
HTTP 통신 횟수 1 ConnectionPool 설정에 따라 다름
ConnectionPool X O

 

이제 DefaultHttpClient에서 발생했던 예외의 원인과 생명주기는 무엇이고, CloseableHttpClient의 커넥션 풀이 어디서 설정되는지 확인해야한다. 이는 생성부분의 내부 코드를 확인해야한다.

 

이는 다음 포스팅에 정리하도록 하겠다.

 

- 혹, 글의 내용 중 맞지 않는 부분이나 수정할 사항이 있다면 꼭 댓글 부탁드립니다. 정말 감사히 받아드리겠습니다!

반응형
반응형

1. 개요

 코딩테스트를 하면 자주 나왔던 Iterator. 이게 무엇인지, 또 왜 사용하는지 알아보았다.


2. Iterator란?

 Iterator란 자바의 컬렉션(Collection)에 저장되어 있는 요소들을 순회하는 인터페이스이다.


3. Collection?

 Collection이란 자바에서 제공하는 자료구조들의 인터페이스로 List, ArrayList, Stack, Quque, LinkedList 등이 이를 상속받고있다. 즉, 이러한 컬렉션 인터페이스를 상속받는 클래스들에 대해 Iterator 인터페이스 사용이 가능하다.

Collection 구조 / 출처 : 위키백과


4. 사용 이유

 컬렉션 프레임워크에 대해 공통으로 사용이 가능하고 사용법이 간단하기 때문이다.

 저 위 그림에 나와있는 클래스, 인터페이스에서 모두 사용이 가능하다.

 

 Iterator를 사용하려면 정의 방법과 메서드 3개만 알면 된다.

 

 정의방법은 Iterator<T> iterator = Collection.iterator(); 이고,

 메서드는 다음 요소가 있는지 판단하는 hasNext(), 다음 요소를 가져오는 next(),  가져온 요소를 삭제하는 remove()가 끝이다. 아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IteratorTest {
 
    public static void main(String[] args) {
        
        List<Integer> list = new ArrayList<Integer>();
 
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        Iterator<Integer> iter = list.iterator();
        
        while(iter.hasNext()) {
            int data = iter.next();
            System.out.print(data);
        }
        
    }
    
}
cs

5~9번 라인에서 Collection 인터페이스를 상속받는 ArrayList 객체를 생성하고 0부터 100 값을 add 한다.

11번 라인에서 Iterator<T> iterator = Collection.iterator(); 형식에 맞게 Iterator<Integer> iter = list.iterator(); 를 사용하여 Iterator를 참조한다.

13번 라인에서 hasNext() 메서드를 사용하여 다음 요소가 있는지 확인한다. (있으면 true, 없으면 false를 반환)

14번 라인에서 next() 메서드를 사용해 다음 요소의 값을 조회한다.


5. Iterator과 반복문

 Iterator를 통한 순회는 반복문을 통한 순회와는 메모리적으로 중요한 차이가 있다.

LinkedList를 통해 예를 들어보겠다.

 

1
2
3
4
5
6
7
8
9
10
11
    public void linkedListTest() {
        LinkedList<Integer> list = new LinkedList<Integer>();
        
        for(int i = 0;i <= 100; i++) {
            list.add(i);
        }
        
        for(int i = 0; i<= 100; i++) {
            list.get(i);
        }
    }
cs

 

Linked List의 메모리구조

add 메서드를 이용해 데이터 입력이 다 끝나면 위 그림과 같은 구조가 된다.

그리고 get(0)부터 get(100)까지를 수행하게 되는데 이는 0부터 100까지 총 101번의 요소를 조회하는게 아니다.

get(int index) 메서드는 시작 주소부터 index 만큼 요소들을 밟아가며 조회하는 메서드이기 때문이다.

만약 5번째 값을 조회한다면 처음 시작주소부터 시작하여 다음주소를 타고... 타고.. 를 총 5번 반복해야한다.

get 메서드가 실행되며 i 값이 증가할 때마다 메모리적으로 조회해야 하는 요소는 1번, 2번, 3번, 4번... 101번까지 증가하는 것이다. 총 5151번을 조회해야 한다.

 

이에반해 Iterator는 1부터 101번째까지의 요소에 대해 내부적으로 객체로 생성한 후 순차적으로 조회한다.

처음 주소로 돌아갈 필요가 없기때문에 next 메서드를 통해 조회 시 요소의 개수인 101번만 조회를 하게된다.

 

그렇다면 드는 생각. 속도면에서 훨씬 빠르지않을까?

 

훨씬 빠를것이라고 생각했으나... Iterator를 구현하기 위해 객체를 생성하는 부분에서 시간이 더 걸린다고 한다.

물론 그 차이는 크지 않지만...

 

결론.

 Iterator는 컬렉션 프레임워크에 대한 인터페이스이고, 사용법이 쉽다.

 하지만 반복문보다 속도면에서 조금 느리다는 평이 있다.

반응형
반응형

1. 개요

 - List에 대해 알고, 배열과 연결리스트의 개념과 차이를 이해한다.

 

2. 리스트(List)란?

 - 순서가 의미를 갖는 데이터들의 집합이다.

 - 삽입, 삭제, 검색 등의 기본적인 연산이 가능한 자료구조이다.

 - 리스트를 구현하는 대표적인 두가지 방법은 배열과 연결리스트이다.

 

즉, 리스트란 첫번째에는 A가, 두번째에는 B가 세번째에는 C가 들어있는 것처럼 순서마다 특정 데이터를 갖고 있고, 이 순서를 통해 삽입, 수정, 삭제, 검색을 할 수 있는 자료구조를 말한다.

 

3. 배열이란?

 - 같은 종류의 데이터들이 메모리상에 순차적으로 저장되어 있는 자료구조이다.

 - 같은 종류의 데이터들이기때문에 각각 데이터들의 크기가 같고, 메모리상에 순차적으로 저장되어 있기 때문에 주소 값 계산이 쉽고, 랜덤 액세스가 가능하다.

 - 조회할때는 빠르나 중간 데이터를 삭제, 추가 시에는 시간이 많이 걸린다.

 - 배열의 구조는 다음과 같다.

배열의 구조

 - int 형은 정수형 자료형으로 4Byte이다. 주소를 보면 4byte 간격이란 것을 알 수 있다. 내가 배열의 5번째 값을 조회하고 싶다면 처음 주소 값에서 자료형 크기 * 5 해준 값(주소값)을 구한 후 바로 접근하면 된다. 즉, 배열에서 특정 데이터를 조회하려면 자료형과 인덱스를 통해 주소 값 계산 후 바로 접근한다. (참고로 이 방식이 랜덤 액세스 방식이다.) 대신 중간에 데이터를 추가하거나 삭제 한다면 빈 자리를 매꾸기 위해 아주 많은 데이터들이 움직여야한다. 예를들면, 3번째 인덱스에 위치한 4000 값을 삭제한다면, 5000이 그 그 자리로 이동해야하고, 5000 자리로는 6000이 이동해야하고... 이게 끝까지 반복되어야한다. 만약 100000개 크기의 배열에서 1번째 값을 삭제한다면 100000-1번 움직여야한다.

 

4. 연결리스트

 - 같은 종류의 데이터들이 메모리상에 비순차적으로 저장되어 있는 자료구조이다.

 - 크기의 제한이 없다.

 - 다른 데이터의 이동 없이 중간에 삽입, 삭제가 가능하다. 

 - 랜덤 엑세스가 불가능하다.

 - 연결리스트의 구조는 다음과 같다.

 - 연결리스트의 값에는 총 2가지 정보가 들어간다. 하나는 메모리 주소에 해당하는 데이터. 하나는 다음 주소 값이다. 그리고 이 주소 값을 거칠때마다 인덱스가 증가한다. 예를들어 이 연결리스트의 2번째 인덱스에 해당하는 값을 조회하려면 시작주소인 00ffff00에 저장된 다음 주소인 00ffff08이 된다. 만약 100000개 크기의 연결리스트에서 100000번째 값을 조회하려면 주소 계산이 되지 않기 때문에 무작정 메모리 시작주소부터 다음 주소를 100000번 까야한다. 즉 조회에 시간이 오래걸린다. 대신, 어떤 값을 삭제하거나 추가할때에는 메모리를 찾은 후 다음주소만 바꿔주면 되기 때문에 배열에 비해 시간이 적게 걸린다. 예를들어 3번째와 4번째 사이에 3500을 추가하고싶다면 먼저 3번째 주소를 찾은 후(00ffff18) 메모리 빈 공간(00ffff10)에 3500값을 만들고 3500의 다음 주소를 3번째 인덱스의 다음 주소로 저장한다. 그리고 3번째 인덱스의 다음 주소에는 3500값의 주소로 저장한다.

 

3500 추가

 

5. 마치며

 가려운 등이 긁힌 느낌이다.. java에서 왜 배열 길이를 선언하고 후에 못늘리는지,(메모리에 순차적으로 들어가기 때문에 나중에 길이를 늘리게 되면 현재 다른 데이터를 위해 할당된 메모리 주소를 침범할 수 있기 때문) 굳이 길이 제한이 없는 ArrayList같은 연결리스트를 놔두고 왜 배열을 쓰는지 (랜덤 액세스로 조회 시간이 엄청 짧음), 그리고 이 둘의 차이, 메모리 구조 등을 확실히 이해할 수 있는 좋은 공부였다. 너무좋다...ㅎㅎ

반응형
반응형

1. 개요

Generic 프로그래밍, Generic 프로그래밍을 사용하는 이유를 예제를 통해 알아보자.


2. Generic 프로그래밍이란?

 제네릭 프로그래밍이란 하나의 데이터가 특정 데이터 타입에만 종속되지 않고 여러 데이터 타입을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그램 방식이다. - 위키백과

 

 무슨 말인지 이해가 잘 안간다. 일단 Generic이 무슨 의미를 갖는지 확인해보자.

 

 Generic의 사전적 의미는 '포괄적인, 총칭, 일반적인' 이다.

 

 이를 Generic 프로그래밍의 정의와 혼합하여 생각해보니 '데이터를 포괄적으로 사용할수 있도록 하는 프로그래밍, 어떤 데이터 타입도 가질 수 있도록 일반화시키는 프로그래밍' 으로 이해를 해보았다.

 

 그렇다면 데이터를 포괄적으로 사용한다는 것이 프로그래밍에서 어떤 의미를 가질까?

 어떤 데이터가 A가 될수도, B가 될수도, C가 될수도 있다라고 생각이 되는데... 예제를 통해 이해해보자.


3. 예제

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box<T> {
 
    private T t;
    
    public void set(T t) {
        this.t = t;
    }
    
    public T get() {
        return t;
    }
}
cs

이 Box 클래스는 제네릭한 클래스이다. T란 녀석이 중간 중간 껴있는 것을 확인할 수 있는데,

T는 내가 생성한 클래스도, 타입도 아니다. Generic 한 클래스를 만들기 위해 사용하는 제네릭 변수이다.

 

Box 객체 생성시 제네릭 변수 T로 들어온 데이터 형을 아래와 같이 변환(?) 시켜준다.

main 메서드의 예제를 보면 이해할 수 있을 것이다.

Generic Class

 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
public class Main {
 
    public static void main(String[] args) {
        Box<Integer> box = new Box<Integer>();
        box.set(10);
        
        Integer i = box.get();
        System.out.println(i.intValue());
    }
}
 
cs

다음과 같이 Box<Integer> box = new Box<Integer>() 로 생성하면, 앞서 이미지의 T 값이 Integer가 되는 것이다.

그럼 Box 클래스는 Integer 타입의 데이터를 관리할 수 있는 객체로 활용되게 된다.

 

Box<String> box = new Box<String>() 으로 생성하면 Box 클래스는 String 타입의 데이터를 관리하고, Human 으로 생성하면 Human 타입의 데이터를 관리하게 된다. 이제 Box라는 클래스가 특정 데이터 타입에 종속되지 않는 Generic한 클래스가 된 것이다.

 

그래도 이해가 잘 안된다면 ArrayList를 생각해봐도 좋을 것 같다.

ArrayList 생성 시 new ArrayList<String>, new ArrayList<Integer>, new ArrayList<Human> 형태로 작성하게 되는데, 입력한 데이터 타입에 따라 add할 수 있는 데이터 타입이 정해지게 된다. Generic과 비슷한 형태와 성질을 띄는 것 같지 않는가?

 

덧붙이면 예제에는 제네릭 변수를 T로 사용했는데 굳이 T가 아니어도 된다. K, V, A 등의 값으로 사용해도 된다.

 

Pair.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Pair<K,V> {
 
    private K key;
    private V value;
    
    public void set(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
}
cs

Pair 클래스의 제네릭 변수를 K, V로 하여 클래스를 작성하였다.

그리고 main 메서드에서 K에는 String 타입을, V에는 Integer 타입으로 Pair 객체를 생성하였다.

 

1
2
3
4
5
6
7
8
9
10
public class Main {
 
    public static void main(String[] args) {
 
        Pair<String, Integer> pair = new Pair<String, Integer>();
        pair.set("test"9);
        System.out.println(pair.getKey()); // test
        System.out.println(pair.getValue()); // 9
    }
}
cs

 

그런데 생각해보니 Object로 대체해도 Generic한 클래스가 되지 않을까?

Box 클래스의 T를 빼버리고, Object 타입으로 넣으면 Object 타입은 모든 클래스의 슈퍼클래스이기 때문에 정형화시킬 수 있지 않는가? 그런데 왜 굳이 Object를 쓰지 않고 Generic을 사용할까?

 


4. Generic 사용 이유

Box.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Box {
 
    private Object t;
    
    public void set(Object t) {
        this.t = t;
    }
    
    public Object get() {
        return t;
    }
}
cs

제네릭 변수를 빼고 Object로 치환하였다.

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        Integer i = (Integer) box.get();
        System.out.println(i.intValue());
    }
}
cs

그리고 main 메서드에서 Box 객체를 생성하였더니 box.get() 부분에서 컴파일 에러가 발생하였다.

Integer i = box.get() 은 슈퍼클래스인 Object를 서브 클래스인 Integer가 참조하려는 형태이기 때문에 에러가 발생한다.

그래서 강제로 캐스팅하는 코드를 추가해주고 있다.

 

반면에 Generic 클래스를 사용하면 캐스팅하는 코드가 없다. Object를 사용하는 것보다 효율적이다.

 

추가적으로 Generic을 사용하면 컴파일 시점에 잡을 수 없었던 타입 에러를 검출 할 수 있다. 

 

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
 
    public static void main(String[] args) {
 
        Box box = new Box();
        box.set(10);
        
        //Integer i = box.get(); // 컴파일 에러
        String i = (String) box.get();
        System.out.println(i);
    }
}
cs

다음과 같이 9행을 String으로 수정하였다.

box.get 이라는 메서드는 Object 형태의 데이터를 return 하기 때문에 컴파일 시점에서는 Integer로 받아도, String으로 받아도 상관없다. 그래서 컴파일 에러가 발생하지 않는다. 하지만 런타임 시 10이라는 정수 타입의 변수라 들어가게 되고, 9번 라인에서 Integer를 String 타입으로 변환하려 하니 castException이 발생하게 된다.

castException

Generic은 캐스팅 할 필요가 없기 때문에 이러한 문제가 발생하지도 않는다.

 

이처럼 Generic은 Object를 사용하는 방법보다 효율적임을 알 수 있다.

 


5. 마치며

오늘은 Generic에 대한 개념을 잡자는 목적으로 포스팅을 하였는데, 목적을 달성했다는 느낌이 든다.

기부니가좋다.

 

반응형

+ Recent posts