Redis란?

백엔드 공부를 하면서 성능을 더 높이기 위해 빠르게 조회용으로 쓰는 NoSQL에 대해서 많이 들어봤습니다. 그 중에서 대표적인 몽고 DB와 Redis가 유명한데 Redis가 무엇인지 궁금하여 찾아봤습니다.

1. Redis의 특징

Redis는 In-Memory 기반의 키-값 형식의 비정형 데이터 구조를 가졌습니다. 따라서 별도의 쿼리 없이도 데이터를 간단히 조회할 수 있습니다.

Redis는 크게 String, Set, Sorted Set, Hash, List 자료구조를 지원하고, 서비스의 특성에 따라서 캐시로도 사용가능하고, Persistence Data Storage로 사용할 수도 있습니다.

2. 캐시 사용의 이점

서비스 요청이 증가할수록, 특히 DB에서 데이터를 조회하는 경우에는 많은 부하를 줄 수 있습니다. 이런 상황에서 나중에 요청된 결과를 미리 저장해두었다가 빨리 제공하기 위해 캐시를 사용합니다.

Redis는 메모리 기반이기때문에 디스크에 비해서 용량은 적지만 접근 속도는 빠릅니다.

3. 캐시 사용 방법

Untitled Diagram (2)

  • Look aside cache 방식

    • (1) 웹 서버는 클라이언트 요청을 받아서, 데이터가 존재하는지 캐시를 먼저 확인합니다.
    • (2) Cache에 데이터가 있으면 조회를 하고, 만약 없으면 DB에서 읽어서 캐시에 저장 후 클라이언트한테 데이터를 전달합니다.
  • Write Back

    • 데이터를 캐시에 전부 먼저 저장해놓았다가 특정 시점마다 한번씩 캐시 내 데이터를 DB에 INSERT 하는 방법입니다.
    • Insert를 1개씩 500번 수행하는 것보다 500개를 한번에 삽입하는 동작이 훨씬 빠르고, write back 방식도 성능면에서는 뒤쳐지지 않다고 합니다.
    • 단점은 데이터를 일정 기간 동안은 유지하고 있어야 하고, 이때 이걸 유지하고 있는 Storage는 메모리 공간이므로 서버 장애 상황에서 데이터가 손실될 우려가 있습니다. 그래서 재생 가능한 데이터나, 극단적으로 무거운 데이터에서 write back 방식을 많이 사용합니다.

4. Redis의 특징

Redis는 Collection을 제공합니다. 주로 게임에서 사용자가 많은 랭킹을 산출할 경우에도 많이 사용하고, Sorted Set을 사용하면 랭킹 서버를 쉽게 구현가능합니다.

Redis Transaction은 한번의 딱 하나의 명령만 수행할 수 있습니다. 이에 더하여 single-thread 특성을 유지하고 있기 때문에 다른 스토리지 플랫폼보다는 이슈가 덜합니다. 하지만 이러한 특징때문에 더블클릭 같은 동작으로 같은 데이터가 2번씩 들어가게 되는 불상사는 막을 수 없기 때문에 별도 처리가 필요합니다.

주로 인증 토큰을 저장하거나 유저 API limit을 두는 상황 등에서 Redis를 많이 사용하고 있습니다.

5. Spring Boot와 Redis 연계하기

간단하게 어떻게 Redis를 설치하고 Spring Boot와 연계하는지 실습을 해봤습니다.

5.1 Docker 설치

먼저, Redis를 Docker를 사용하여 설치하였습니다. 도커는 URL을 통해 설치하시면 됩니다.

Docker 설치하기

5.2 Redis 설치

Redis 설치 및 실행 방법

5.3 의존성(dependency) 추가

api("org.springframework.boot:spring-boot-starter-data-redis")

application.yml 설정 파일에 연동할 Redis의 접속정보를 기술합니다.

spring:  
  redis:
    host: localhost
    port: 6379

스프링 부트는 기본적으로 Redis 포트를 6379로 설정하고 있습니다.

6. Redis 실습

6.1 StringRedisTemplate

저는 Kotlin을 사용하여 Redis의 명령어들을 사용하였습니다. 그 중 strings 타입의 데이터를 저장하는 StringRedisTemplate을 Spring Boot에서 사용하는 예제입니다.

먼저, StringRedisTemplate을 사용하기 위해 Bean을 정의해줍시다.

StringRedisTemplate
@Component
class RedisRunner(val redisTemplate: StringRedisTemplate): ApplicationRunner {

    override fun run(args: ApplicationArguments?) {
        val values = redisTemplate.opsForValue();
        values.set("junyoung", "30")
        values.set("hobby", "coding study")
        values.set("dream", "take a walking with my puppy")
    }
}

StringRedisTemplate을 의존성으로 주입받아서 set으로 key, value를 저장하였습니다.

opsForValue() 메서드는 value관련된 operation들을 제공하는 객체를 받아옵니다.

DokotlinApplication

@EnableJpaAuditing
@SpringBootApplication
class DokotlinApplication(var boardRepository: BoardRepository) {

fun main(args: Array<String>) {
    runApplication<DokotlinApplication>(*args)
}

스프링 부트 어플리케이션을 실행하면 아래와 같이 key-value가 들어간것을 Redis Client에서 확인이 가능합니다.

실행 결과

image

keys * 명령어는 Redis에 저장된 key 값들을 전부 조회하는 명령어로 테스트를 위해서 사용하였지만, 실무에서는 사용하지 않을 것을 권고하고 있습니다.

get hobby 명령어를 실행하면 hobby에 매칭되는 value인 "coding study"가 나오는 것을 확인할 수 있었습니다.

6.2 RedisEntity 실습

이번에는 Entity를 Redis에 저장하는 예제를 살펴보겠습니다.

@RedisHash(value = "fund")
class Fund(
    @Id
    val id: Long? = null

) {
    val name = "name-$id"
}

interface FundRedisRepository: CrudRepository<Fund, Long>

Redis 엔티티 Fund를 정의하였습니다.
@RedisHash("fund")으로 해당 엔티티가 Redis 엔티티임을 명시하였습니다.

@Id 어노테이션이 적용된 맴버변수에 값이 실제 hash_id가 됩니다. 만약 null 값을 주게 된다면 Redis 내부적으로 random한 값을 넣어주게 됩니다.

앞으로 Fund라는 엔티티 데이터들을 Redis에 무수히 저장이 될텐데, 이 엔티티들만 보관하는 하나의 Hash 키 값이 @RedisHash("fund") 이고, 이 Hash 공간에서 각 엔티티들이 fund:hash_id라는 키 값을 가지게 됩니다.

Untitled Diagram drawio (5)

fund:hash_id Key와 하나 이상의 Field/Element 값으로 저장할 수 있으며, Value에는 기본적으로 strings 데이터를 저장할 수 있습니다.

즉, HashMap<String,HashMap<String,Fund>> 구조로 구성되어 있습니다.

이제 테스트 코드를 작성하여 실제 Fund 엔티티들을 Redis에 저장해봤습니다.

@SpringBootTest
class FundRedisRepositoryTests {

    @Autowired
    lateinit var fundRedisRepo: FundRedisRepository

    @Test
    @DisplayName("펀드 엔티티들을 Redis에 저장한다")
    fun saveFundWithRedis() {

        val funds = (1..10).map {
            Fund(
                id = it.toLong()
            )
        }
        fundRedisRepo.saveAll(funds)
    }
}

Redis를 GUI로 쉽게 접근할 수 있는 Medis라는 Tool을 사용해서 결과를 확인해봤습니다.

실행 결과

image

짜잔... Fund 엔티티들이 Redis에 저장되었습니다. 아래 이미지를 보면 Redis에 총 11개의 key 값이 존재합니다.

image

fund는 위에서 Redis 엔티티를 정의할 때 작성한 @RedisHash("fund")를 의미합니다. 그 외에 뒤에 붙은 1 ~ 10까지의 숫자들은 hash_id를 의미합니다.

image

타입을 살펴보면 fund key는 Set 타입이고,
fund:hash_id key는 Hash 타입입니다.

6.3 Redis Hash 타입 명령어

Hash 타입의 데이터를 처리할 때는 hmset, hget, hgetall, hkey, hlen 명령어를 사용합니다.

대표적으로 아래 3개의 명령어를 찾아봤습니다.

  • hget hash_id field: 해당 key에 대한 필드의 value를 검색

  • hgetall hash_id: 해당 key에 대한 모든 field와 value들을 검색

  • hexists hash_id field: 해당 key에 대한 field가 존재하는지 여부 확인

image

참조 사이트: https://velog.io/@max9106/Spring-Boot-Redis,https://coding-start.tistory.com/130, https://cheese10yun.github.io/redis-getting-started/

'SpringFramework > Spring Boot' 카테고리의 다른 글

Spring RestDocs  (0) 2021.05.11
멀티 모듈에서 QueryDSL 설정 방법  (0) 2021.05.11
HandlerMethodArgumentResolver란?  (0) 2019.12.16
인터셉터란?  (0) 2019.12.07
JWT 토큰 기반 인증  (0) 2019.12.06

Jenkins SSH를 이용한 GitHub 연동방법

젠킨스와 GitHub를 연동하여 프로젝트 소스를 가져와서 빌드하는 방법이 여러가지가 있지만, 사용자명과 비밀번호 인증방식은 보안상 추천하지 않기 때문에 SSH기반으로 젠킨스와 GitHub를 연동하는 방법에 대해서 직접 구축해봤습니다.

Jenkins 설치가 안되어있다면 Jenkins 설치 방법를 참조하시면 됩니다.

1. 키파일 생성

먼저, Jenkins가 실행중인지 확인합니다.

ps aux | grep jenkins

image

구글링을 통해서 키 파일 생성방법을 찾을 때 jenkins 권한을 가진 jenkins 계정으로 접속하라는 글을 보았지만, 저 같은 경우에는 제 별도의 계정으로 jenkins를 실행했기 때문에 jenkins라는 계정이 존재하지 않았습니다. 실제로 실행중인 프로세스를 보면 limjun-young 계정으로 jenkins가 실행되는 것을 볼 수 있습니다.

그럼 본격적으로 키 생성을 위해 bash 쉘로 접속합니다.

sudo -u limjun-young /bin/bash

image

.ssh 디렉토리를 하나 생성합니다.

mkdir /var/lib/jenkins/.ssh
cd /var/lib/jenkins/.ssh

혹시 디렉토리 생성 권한이 없다고 나오면 sudo를 앞에 붙여서 다시 실행합니다.

이제 여기서 ssh 키를 생성합니다.

ssh-keygen -t rsa -f /var/lib/jenkins/.ssh/github_ansible-in-action

image

비밀번호는 모두 입력없이 바로 Enter로 넘어갑니다.

위와 같이 터미널 콘솔에 출력되었다면 키들이 RSA 공개키/비밀키가 잘 생성되었는지 확인합니다.

image

2. GitHub

이제 젠킨스로 연동하고 싶은 GitHub의 Setting 페이지로 이동합니다.

Deploy keys -> Add deploy key 클릭 하면 아래와 같이 공개키를 입력하는 폼 화면이 나옵니다.

스크린샷 2021-05-19 오후 7 28 07

젠킨스 서버에서 생성한 공개키 코드를 복사해서 붙이면 됩니다.

공개키 등록

cat /var/lib/jenkins/.ssh/github_ansible-in-action.pub
bash-3.2$ cat github_ansible-in-action.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCnLB6lRaWdLRCDjhoIj1n9BYWd7oTCKbKQXuaxH82tkhFmmgL4VNPlUZV+TKLaQpyQN3ZxG86fGXNQFmuDRdmuJvLtQyeVY2abKkv9KFvmpeUV1++G7JBduxV9WU7qtlGt/LoR+s9Wvlv9Tc6hdDMiTg/jKSK5Ppk5d3HUotTQNfncPYzU6lqyZhR9x+uUhEUKObh3oGi0Xz+x2VHEYrDYpnWQPOsmYJbH8uqA12coQyL+9YPt4wfbuqsGl9qUy3eS7YkRCQQCQXYUiTibPvz6DLieWKF+EO/lVZO/3rCAvX9EI4zhre2ATDLVOjqMmWnL+Db3OoAjGCOxz5FuIm6CojKNdj2BJdadipd8Qop+uPbBnkePnYSx4b4Arlh5OFSONdeyxTV5CoF+NTQCzGRcc+Yj3sAbOKZvU3qX5NwBZRSUskvla1w3Bg+ybU59f5sHc5PuHAF46BdS28J2tJ6Sep0c8nMeZTNJjAqAhlJQ9NjmHViQ2kRzxt1nnre7mD8= root@imjun-yeong-ui-MacBookPro.local

출력된 코드를 key 항복에 붙여넣습니다.

image

3. Jenkins

젠킨스 웹 화면에서 Jenkins 관리 -> Security의 Manage Credentials -> Stores scoped to Jenkins의 Domains의 global 클릭 -> 왼쪽 사이드바의 Add Credentials 클릭합니다.

비밀키 등록

image

비밀키를 등록하기 위해 젠킨스 서버에 있는 비밀키를 복사합니다.

cat /var/lib/jenkins/.ssh/github_ansible-in-action
  • kind
    • 인증 방식을 선택합니다.
    • 여기선 비밀키 방식을 선택해야 GitHub의 공개키/비밀키로 인증이 가능합니다.
  • Username
    • 각 젠킨스 Job에서 보여줄 인증키 이름입니다.
    • 키 이름 그대로 사용해도 됩니다.
  • Private Key
    • 복사한 비밀키를 그대로 붙여넣습니다.

이제 고대했던 젠킨스와 GitHub 연동이 끝났습니다. 실제로 ssh 연동이 잘 되는지 확인이 필요합니다.

Jenkins 웹 화면에서 새로운 Item을 클릭합니다.

image

FreeStyle 프로젝트를 생성합니다.

image

GitHub의 코드를 가져올 수 있는지 소스코드 관리 항목에 입력합니다.

image

  • Repository URL에는 공개키를 등록한 URL을 등록합니다.
  • Credentials에는 이전에 등록한 젠킨스 인증 Username을 선택합니다.
  • 브랜치는 기본 브랜치인 master를 등록합니다.

이제 저장 후 GitHub에 있는 소스코드를 Build 해보겠습니다.

아래와 같이 GitHub에서 코드를 가져오는 로그가 출력된다면 연동이 성공한 것입니다.

image

4, Build 결과물

Build가 성공했으니 실제 결과물을 확인해야 합니다. 저는 FreeStyle 프로젝트 생성 시 입력한 타이틀을 github로 하였습니다. 작업공간을 클릭합니다.

image

짜잔!! 실제 GitHub에서 가져온 소스코드를 빌드한 결과물을 zip 파일로 압축이 가능해졌습니다.

image

이제 개발할 때마다 깃허브에 PUSH를 하면 Jenkins에서 변경된 사항에 대해서 Build가 가능해집니다.

참조 사이트: https://jojoldu.tistory.com/442

'SpringFramework' 카테고리의 다른 글

Jenkins에서 EC2로 배포하기  (0) 2021.05.20
Jenkins 설치 및 구동하기  (0) 2021.05.19
Remember - Me 인증하기  (0) 2019.11.18

Jenkins에서 EC2로 배포하기

GitHub와 젠킨스를 SSH로 연동했다면, 이제 AWS(Amazone Web Service)에서 제공하는 가장 유명한 EC2(Elastic Compute Cloud)에 배포를 해봤습니다.

1. Publish Over SSH 플러그인 설치

먼저, Jenkins에서 제공해주는 플러그인 중에 SSH로 EC2에 jar 배포 파일을 전달을 해주는 Publish Over SSH 플러그인을 설치해야 합니다.

플러그인 다운로드

왼쪽 사이드 바에서 Jenkins 관리에서 플러그인 관리를 클릭합니다.

스크린샷 2021-05-20 오후 6 32 45

저는 이미 설치를 한번 했기 때문에 설치된 플러그인 목록에서 Publish Over SSH를 보여주지만, 처음 설치하시면 설치 가능에서 Publish Over SSH 검색하시면 됩니다.

image

2. 시스템 설정

Jenkins 관리에서 시스템 설정을 클릭합니다.

image

맨 하단에 Publish over SSH라는 폼 화면이 보이게 됩니다.

image

Key에는 AWS EC2 인스턴스 생성 시 발급받은 RSA 키가 필요합니다. EC2 생성할 때 받은 ssh 접속 키인 pem 파일내용을 붙여넣기를 하면 됩니다.

image

  • Name: 본인이 사용할 임의의 SSH Server의 Name을 입력하면 됩니다.
  • Hostname: 실제로 접속할 원격 서버 ip, 접속 경로를 입력합니다. ex) 퍼블릭 IPv4 주소: 3.37.87.X
  • Username: 접속할 원격 서버의 user 이름입니다. ex) ec2-user입니다.
  • Remote Directory: 원격서버에서 접속하여 작업을 하게 되는 디렉토리 입니다.

Test Configuratoin을 눌러서 Success라고 문구가 나오면 성공적으로 SSH 설정이 되었습니다.

3. Jenkins Item 구성

이제 SSH를 통해 배포 jar 파일을 보낼 준비가 거의 다 되었습니다.
아래 Jenkins Item을 클릭 후 구성 버튼을 누릅니다.

image

Build

Build 항목은 하단의 셀렉트 박스를 통해서 빌드 시 할 수 있는 작업을 정의해 놓았습니다.

image

젠킨스 통합환경 설정에서 Gradle Toll Configuration에서 Gradle을 다운받아 전역적으로 사용이 가능합니다.
저는 GitHub에 Gradle wrapper를 push 하였기 때문에 Invoke Gradle script 방식으로 Build를 설정하였습니다.

Use Gradle Wrapper를 선택하고, Tasks에 clean 후 Build를 수행하도록 설정하였습니다. 만약 test도 수행하고 싶으면 스프링 부트 프로젝트의 build.gradle 파일에 아래 설정을 추가하면 됩니다. 보통은 기본적으로 추가가 되어있습니다.

test {
    useJunitPlatform()
}

빌드 후 조치

이제 스프링 부트 어플리케이션 배포 파일을 보내는 설정을 하고 어떻게 실행할지 정의하는 부분을 입력해야 되는데.. 이게 가장 햇갈렸지만 이미 다른 개발자분들의 좋은 블로그 글을 통해서 금방 이해할 수 있었습니다.

image

  • Source files: 내가 전송할 파일의 위치를 적습니다. Jenkins의 workspace 기준으로 적게 되는데 git pull로 당겨온 스프링 부트 앱의 루트 경로라고 생각하면 됩니다. jar 파일의 위치는 build/libs 하위에 있습니다. 이 경로를 jar 파일이라고 명시를 해줍니다.
  • Remote Directory: 파일을 전송할 원격 서버의 디렉토리를 명시하는 부분입니다. 여기서 주의할 점은 위에서 SSH 서버 설정하는 부분에서 이미 경로(/home/ec2-user)를 적어줬기 때문에 그 밑의 경로부터 적어야 합니다. 저는 /deploy로 적었기 때문에 빌드 후에 ec2 서버의 /home/ec2-user/deploy 하위로 jar 파일이 배포가 됩니다.
  • Exec command: 파일 전송 후 실행할 명령어를 입력합니다. 절대 경로로 ec2 루트경로(/home/ec2-user/start_server.sh)부터 적었습니다.

배포 후 실행될 스크립트 명령어

!#/bin/bash

echo "Start Spring Boot Application!"
CURRENT_PID=$(ps -ef | grep java | grep dokotlin | awk '{print $2}')
echo "$CURRENT_PID"

 if [ -z $CURRENT_PID ]; then
echo ">현재 구동중인 어플리케이션이 없으므로 종료하지 않습니다."

else
echo "> kill -9 $CURRENT_PID"
kill -9 $CURRENT_PID
sleep 10
fi
 echo ">어플리케이션 배포 진행!"
nohup java -jar /home/ec2-user/deploy/dokotlin-0.0.1-SNAPSHOT.jar >> /home/ec2-user/deploy/logs/dokotlin.log &

bash 쉘을 이용하여 작성한 스크립트로 단순히 grep 명령어로 실행 중인 부트 애플리케이션의 pid (프로세스 식별자)를 조회하여 길이가 0이면 앱을 실행시키고, 만약 떠있는 앱이 존재하면, kill -9 명령어로 강제로 내린 후에 다시 재기동을 하도록 작성하였습니다.

image

Name 하위에 고급 버튼을 클릭후 Verbose output in console을 체크하면 빌드할 때 상세 내역이 표시되므로 유용합니다.

4. Jenkins 빌드 결과물

이제 Jenkins로 다시 Build를 수행하면 /home/ec2-user/deploy/ 디렉토리 하위에 jar 파일이 떨어지고, 파일 전송 후 실행할 어플리케이션 기동 명령어가 저장되어 있는 start_server.sh이 실행됩니다.

아래 젠킨스 콘솔 로그에서 확인 가능합니다.

image

EC2 서버에 배포된 jar 파일

image

EC2 서버 스프링 부트 어플리케이션 실행 화면

image

만약에 EC2 서버 로컬에서 정상적으로 웹 API 호출이 되지만, 내 PC 로컬 브라우저에서 화면이 안보인다면 아래와 같이 EC2 Security Group에서 인바운드 규칙을 살펴봐야 합니다. 저같은 경우에는 스프링 부트 어플리케이션을 8080 포트로 실행시켰기 때문에 인바운드 규칙에 TCP 8080포트를 허용하도록 설정했습니다.

인바운드 규칙

image

참조 사이트: https://pjh3749.tistory.com/261, https://goddaehee.tistory.com/259?category=399178

'SpringFramework' 카테고리의 다른 글

Jenkins SSH를 이용한 GitHub 연동방법  (0) 2021.05.20
Jenkins 설치 및 구동하기  (0) 2021.05.19
Remember - Me 인증하기  (0) 2019.11.18

Jenkins 설치 및 구동하기

대표적인 CI 도구인 Travis-CI와 젠킨스가 있지만, 그 중에서 오래된 역사와 다양한 플러그인을 제공해주는 Jenkins를 설치하는 방법에 대해서 살펴보았고, 직접 저의 로컬환경에서 구축해봤습니다.

설치환경

  • Mac OS
  • 패키지 관리자 homebrew

1. Jenkins 설치

먼저, GitHub와 연동해야 할 대표적인 CI 툴인 젠킨스를 설치해야 합니다.
제 PC 환경은 Mac OS이기 때문에 패키지 관리자인 homebrew를 통해서 설치하였습니다.

Jenkins 설치 명령어

brew install jenkins
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

혹시 설치가 정상적으로 안되고 위와 같은 에러로그가 찍히면 xcode-select --install를 설치하시면 됩니다.

Jenkins 포털 설정 화면

unlock jenkins

Administrator password를 입력하면 되는데 패스워드는 아래 경로에 있는 파일을 열어서 복사 후 입력하시면 됩니다.

$ vi /var/lib/jenkins/secrets/initialAdminPassword

플러그인 설치 화면

패스워드를 입력 후 아래와 같이 플러그인 설치화면이 나오는데 특별히 설치할 플러그인이 필요없다면 install suggested plugins를 선택합니다.

jenkins install

Admin 계정 생성

이제 Admin 사용자를 생성하고 다음 화면을 누르면 Jenkins 화면이 보이는 것을 확인할 수 있습니다.

스크린샷 2021-05-19 오후 4 18 20

2.외부 IP로 접근 가능하도록 설정

해당 Mac 호스트를 CI 전용 서버로 쓰는 경우 localhost:8080 이외의 도메인이나 공인 IP, 외부 IP로 접속하기를 원할 수 있습니다. 이 경우에는 아래와 같은 경로에서 --httpListenAddress 값을 127.0.0.1에서 0.0.0.0으로 변경해야합니다. 만약 Jenkins 포트번호도 변경하고 싶다면 --httpPort={port번호}를 수정하면 됩니다. 그럼 이제 공인이나 사설 IP로도 Jenkins로 접근이 가능해집니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>homebrew.mxcl.jenkins</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/libexec/java_home</string>
      <string>-v</string>
      <string>1.8.0_201</string>
      <string>--exec</string>
      <string>java</string>
      <string>-Dmail.smtp.starttls.enable=true</string>
      <string>-jar</string>
      <string>/usr/local/opt/jenkins/libexec/jenkins.war</string>
      <string>--httpListenAddress=0.0.0.0</string>
      <string>--httpPort=8888</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
  </dict>
</plist>

-Dmail.smtp ~ 위에 설정한 JDK 버전 값은 만약 /usr/libexec/java_home -V에서 여러개의 JDK를 설치된 경우 특정 JDK 버전을 지정하기 위해서 명시한 설정 값입니다. 저 같은 경우에는 JDK가 2개가 설치되어 있어서 1.8.0_201버전으로 Jenkins의 Job을 구동하도록 설정하였습니다.

3. Jenkins 웹 UI 화면

만약 저처럼 위 설정파일에서 젠킨스 실행포트를 8888로 변경하지 않았다면 기본인http://localhost:8080로 접속하면 아래와 같이 젠킨스 웹 화면이보이게 됩니다.

스크린샷 2021-05-19 오후 6 58 04

참조 사이트: https://wan-blog.tistory.com/74?category=776763

'SpringFramework' 카테고리의 다른 글

Jenkins SSH를 이용한 GitHub 연동방법  (0) 2021.05.20
Jenkins에서 EC2로 배포하기  (0) 2021.05.20
Remember - Me 인증하기  (0) 2019.11.18

Spring Rest Docs란?

Spring Rest Docs는 테스트 코드를 기반으로 자동으로 API 문서를 작성할 수 있게 도와주는 프레임워크입니다.

그렇기 때문에 반드시 Test가 통과되어야 문서가 작성 된다는 장점이 있습니다.

Test 통과과 전제조건이기 때문에 API Spec이 변경되거나 추가/삭제 되는 부분에 대해 항상 테스트 코드를 수정해야되고, API 문서가 최신화 될 수 있도록 해줍니다.

처음에는 마크다운이 저에게 익숙하기 때문에 마크다운으로 API 문서를 작성하려고 했지만, 설정을 하는 부분도 번잡하고, 대부분의 큰 서비스 회사에서도 asciidoc을 채택하는 것 같아서 asciidoc을 사용하여 문서를 작성하기로 했습니다. asciidoc은 마크다운과 비슷하게 html를 작성할 수 있는 언어입니다.

1. Spring Rest Docs 설정 방법

일단, 의존성 라이브러리로 JUnit, MockMvc, asciidoc을 선택하여 최소한의 라이브러리로 구현하였습니다.

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.2.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id "org.asciidoctor.convert" version "1.5.9.2" // (1)
}

group 'com.kakaopay'
version '1.0-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // (5)
}

test {
    useJUnitPlatform()
}

// (2)
asciidoctor {
    dependsOn test  
}

// (3)
bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}/html5") {
        into "BOOT-INF/classes/static/docs"
    }
}

// (4)
task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/asciidoc/html5")
    into file("src/main/resources/static/docs")
}

// (6)
build {
    dependsOn copyDocument
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

dependsOn은 종속 테스크를 지정할 때 사용하는 기능입니다.

테스크 순서

  1. (1), (2)는 asciidoc 파일을 컨버팅하고 Build 디렉토리로 복사하기 위한 플러그인 입니다. asciidoctor Task를 통해 html 문서로 processing 되어 build/asciidoc/html5 하위에 html 문서로 생성이 됩니다.
  2. (3)은 gradle build 시에 asciidoc -> bootJar 순으로 수행이 되고, (4)은 실제 배포 시, BOOT-INF/classes가 classpath가 되기 때문에 아래와 같이 파일을 복사해야 합니다.
  3. (5)는 mockMvc를 restdocs에 사용할 수 있게 하는 라이브러리 입니다.
  4. (6)은 build 테스크를 수행하기 전에 소스 코드에 html파일을 복사하는 테스크 작업입니다.

2. RestDocs 예제 코드

ApiDocumentUtils 클래스

import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;

public interface ApiDocumentUtils {
    static OperationRequestPreprocessor getDocumentRequest() {
        return preprocessRequest(
                modifyUris() // (1)
                        .scheme("http")
                        .host("investment.api.com")
                        .port(8080),
                prettyPrint());
    }

    static OperationResponsePreprocessor getDocumentResponse() {
        return preprocessResponse(prettyPrint());
    }
}

위의 ApiDocumentUtils 클래스의 getDocumentRequest() 메서드는 문서상 uri를 기본 값인 http://localhost:8080에서 http://investment.api.com으로 변경하기 위해 사용합니다.

prettyPrint()는 문서의 request를 보기좋게 출력하기 위해 사용합니다.
getDocumentResponse() 메서드 역시 문서의 response를 보기 좋게 출력하는 용도로 사용합니다.

InvestmentRestControllerTests 클래스

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class InvestmentRestControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Rule
    public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();

    @Autowired
    WebApplicationContext context;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @DisplayName("회원이 특정 투자상품에 투자 요청을 정상적으로 처리한다.")
    @Test
    public void 투자결과_리턴_테스트() throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("itemId", 1);
        jsonObject.put("investingAmount", 100000);
        String jsonBody = jsonObject.toString();

        ResultActions result = mockMvc.perform(post("/api/investment")
                .content(jsonBody)
                .header("X-USER-ID", 1)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON));

        result.andExpect(status().isOk())
                .andExpect(jsonPath("$.result", is("success")))
                .andDo(document("investment",
                        getDocumentRequest(),
                        getDocumentResponse(),
                        requestFields(
                                fieldWithPath("itemId").type(JsonFieldType.NUMBER).description("투자상품"),
                                fieldWithPath("investingAmount").type(JsonFieldType.NUMBER).description("투자 금액")
                        ),
                        responseFields(
                                fieldWithPath("result").type(JsonFieldType.STRING).description("투자 결과")
                        )
                ));
    }
}

기본적으로 MockMvc 기반으로 테스트를 하기 전에 setUp()에서 두 가지 방식으로 진행해야 하는데, 저는 webAppContextSetup을 이용하여 WebApplicationContext의 인스턴스로 작동하도록 셋업을 하였습니다. 이렇게 하면 스프링 컨트롤러는 물론 의존성까지 로드되기 때문에 완전한 통합 테스트를 할 수 있습니다.

나머지 하나는 아래 코드처럼 standaloneSetup()을 사용하여 한 컨트롤러에 집중하여 테스트 하는 용도로만 사용한다는 점에서 유닛 테스트와 유사합니다.

this.mockMvc = MockMvcBuilders
            .standaloneSetup
            .apply(documentationConfiguration(restDocumentation))
            .build()

JUnitRestDocumentation는 JUnit 프레임워크로 RestDocs를 실행하기 때문에 필요한 객체 입니다.

requestFields, responseFields 메서드는 사용자 정의 API 스펙에 따라서 요청 필드, 응답 필드를 정의할 수 있습니다.

requestFields(fieldWithPath), responseFields(fieldWithPath())는 request-fields asciidoc, response-fields asciidoc 파일들을 생성 합니다.

위 테스트 코드가 성공하면 build/generated-snippets/investment 디렉토리 하위에 adoc 파일이 생성되는 것을 확인할 수 있습니다.

image

3. Snippet 문서

RestDocs에 대한 명세내역을 코드에 작성한 후 테스트 케이스가 정상적으로 실행이 되었다면 RestDocs의 내용을 기준으로 Snippet 문서가 생성되는데, Gradle 기준으로 build/generated-snippets 디렉토리 하위에 생성이 됩니다.

이 Snippet이란 문서의 조각을 의미합니다. Gradle 빌드 스크립트에서 asciidoctor로 직접 작성한 문서와 Spring MVC 테스트를 통해 자동 생성된 스니펫을 결합해서 웹 문서를 만듭니다.`

4. Snippet 종류

  • curl-request.adoc : 호출에 대한 curl 명령을 포함 하는 문서
  • httpie-request.adoc : 호출에 대한 http 명령을 포함 하는 문서
  • http-request.adoc : http 요청 정보 문서
  • http-response.adoc : http 응답 정보 문서
  • request-body.adoc : 전송된 http 요청 본문 문서
  • response-body.adoc : 반환된 http 응답 본문 문서
  • request-parameters.adoc : 호출에 parameter 에 대한 문서
  • path-parameters.adoc : http 요청시 url 에 포함되는 path - parameter 에 대한 문서
  • request-fields.adoc : http 요청 object 에 대한 문서
  • response-fields.adoc : http 응답 object 에 대한 문서

이중에서 RestDocs 문서를 만들기 위해 사용될때 가장 우선순위가 높은 문서는 (curl-request.adoc, request-parameters.adoc, path-parameters.adoc, request-fields.adoc, response-fields.adoc) 가 되는데 이 외의 스니펫 문서는 필요에 따라 선택적으로 포함 시켜 줍니다.

5. asciidoc html 파일 작성

이제 Snippet 문서를 html 문서로 생성하기 위해 src/docs/asciidoc/api-guide.adoc 문서를 작성하면 됩니다.

image

참고로 Gradle에서는 Maven이랑 다르게 src/docs/asciidoc 디렉토리 하위에 adoc 문서를 생성해야 합니다. 그렇지 않으면 에러가 발생합니다.

ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
:doctype: investment
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

[[resources]]
= Resources

[[resources-investment]]
== Investment

[[resources-investment]]
=== 정보 가져 오기
include::{snippets}/investment/http-request.adoc[]
include::{snippets}/investment/http-response.adoc[]
include::{snippets}/investment/response-fields.adoc[]
include::{snippets}/investment/request-fields.adoc[]

저 같은 경우에는 외부 API를 사용하기 위해서 정말 필요하다고 생각되는 snippet만 adoc 파일에 포함시켰습니다. include::~ 이 부분이 실제 html 태그안에 삽입하는 부분입니다.

이제 Gradle로 build 테스크를 수행하면 위 스크립트에서 작성한 static/docs/api-guide.html 파일이 생성되는 것을 확인할 수 있고, http://localhost:8080/docs/api-guide.html로 접근이 가능하면 아래와 같은 웹 문서가 렌더링 됩니다.

실행 결과

스크린샷 2021-04-12 오후 3 21 27

참조 사이트: https://jogeum.net/16, https://jaehun2841.github.io/2019/08/04/2019-08-04-spring-rest-docs/#userkt, 우아한 형제 기술 블로그

'SpringFramework > Spring Boot' 카테고리의 다른 글

Spring Boot + Spring Data Redis 연동  (0) 2022.07.10
멀티 모듈에서 QueryDSL 설정 방법  (0) 2021.05.11
HandlerMethodArgumentResolver란?  (0) 2019.12.16
인터셉터란?  (0) 2019.12.07
JWT 토큰 기반 인증  (0) 2019.12.06

멀티 모듈에서 QueryDSL 설정 방법

최근에 Spring Boot를 사용하여 멀티 모듈을 구성하였고, 하위 모듈별로 꼭 필요한 의존성만 설정하도록 했습니다.

기술 스펙은 아래와 같습니다.

  • Spring Boot 2.3
  • Gradle 6.3

1. 멀티모듈 구성 화면

image

core 모듈은 여러 하위 모듈에서 공통으로 사용하는 도메인 계층을 관리합니다.

settings.gradle

gradle이 빌드 과정에서 settings.gradle 파일을 참조하기 때문에 반드시 root 모듈과 sub 모듈을 include 해야합니다.

settings.gradle 파일

rootProject.name = 'yolo'
include 'yolo-core'
include 'yolo-api'

2. QueryDSL 설정 방법

저같은 경우에는 core 모듈에서 도메인 영역인 Entity, Repository를 가지고 있기 때문에 core 모듈에서 QueryDSL 플러그인과 의존성을 가지고 있도록 설정했습니다.

core - build.script

plugins {
    // (1) QueryDSL 플러그인 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

bootJar {
    enabled = false
}

jar {
    enabled = true
}

dependencies {
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
    // (2) QueryDSL 의존성 추가
    compile('com.querydsl:querydsl-jpa') 
}

// (3) QClass 생성 위치
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

// (4) QClass 소스 위치를 지정합니다.
sourceSets {
    main.java.srcDir querydslDir
}
// (5) gradle 5.0 설정
configurations {
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

(1) 플러그인을 사용할 수 있도록 설정합니다.
(2) QueryDSL 의존성 라이브러리를 추가합니다.
(3) QueryDSL를 사용하기 위해서는 QClass라는 도메인의 meta model이 필요한데 QClass를 생성하고 빌드 후 generaged/querydsl 디렉토리 하위로 파일이 생성됩니다.

아래 코드는 QueryDSL 플러그인 저장소 url를 repositories에 선언하여 의존성을 받아오도록 설정한 코드입니다.

repositories {
    mavenCentral()
    maven {
        url "https://plugins.gradle.org/m2/"
    }
}

dependency 추가

만약 멀티 모듈을 사용할 경우, 저처럼 core 상위 모듈에서 QueryDSL을 사용할 경우 테스트 코드는 api 하위 모듈에서 작성하게 됩니다. 그러면 테스트 코드에서 JPAQueryFactory를 주입받아서 repository 계층에 대해서 테스트를 진행하게 되는데, 이 경우에 테스트는 문제없이 성공하지만 build를 실행할 경우 JPAQueryFactory does not exist라는 에러 문구가 나오면서 build가 실패합니다.

여기서 많은 삽질을 하였였는데, 실패 사유는 아래와 같이 build.script에서 implementation으로 선언해서 발생한 문제였습니다. implementation으로 의존성이 등록되면, 하위 프로젝트들은 해당 의존성을 가질 수 없습니다.

즉, 아래와 같이 api 하위 모듈이 core 상위 모듈을 의존하고 있다면 api는 implementation으로 선언된 com.querydsl:querydsl-jpa을 사용할 수가 없습니다. 이렇게 함으로써 멀티 모듈에서의 의존관계에 개방/폐쇄를 적용할 수가 있습니다.

core 모듈 build.script 수정 전

implementation('com.querydsl:querydsl-jpa') 

그래서 저는 아래와 같이 api 모듈에서도 사용할 수 있도록 compile로 변경했습니다.

core 모듈 build.script 수정 후

compile('com.querydsl:querydsl-jpa') 

3. QueryDSL 상세 설정

jpa

아래 설정에서 jpa는 QClass를 자동으로 생성할지 결정합니다.
기본값은 false 입니다.

만약, true일 경우 com.querydsl.apt.jpa.JPAAnnotationProcessor가 추가되면서 프로젝트에 사용됩니다.

JPAAnnotationProcessor는 meta model(QClass)를 생성합니다.

false일 경우 build 타임에 QCalss들이 생성되지 않게 됩니다.

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

querydslSourceDir

  • 어디에 QClass를 생성할지 설정합니다.
  • 기본값은 src/querydsl/java 입니다.
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

sourceSet

sourceSets {
    main.java.srcDir querydslDir
}
  • QClass가 생성된 위치를 나타냅니다.
  • 위에서 선언한 querydslSourceDir가 위치가 같아야 합니다.
  • 기본적으로 자바 소스와 리소스 파일의 그룹을 나타냅니다.
  • 기본적으로 java 플러그인은 src/main/javasrc/test/java를 기본 소스 디렉토리로 인식합니다.
  • main.java.srcDirbuild/generated/querydsl이 되게 됩니다.
  • 나중에 User라는 도메인을 생성하면 해당 위치에 QUser라는 QueryDSL용 도메인이 생성되고 사용하게 됩니다.

4. gradle 5.0 설정

configurations {
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

gradle 5+ 버전을 사용할 경우 위와 같은 설정 값을 추가해야 합니다.

configuration

  • QueryDSL의 컴파일된 클래스 패스 경로를 지정합니다.

compileQuerydsl

  • querydsl-apt의 annotation processor 경로를 지정합니다.
  • gradle 5 버전에서는 자체 annotation processor를 사용합니다
  • querydsl-apt의 annotation processor와 충돌

'SpringFramework > Spring Boot' 카테고리의 다른 글

Spring Boot + Spring Data Redis 연동  (0) 2022.07.10
Spring RestDocs  (0) 2021.05.11
HandlerMethodArgumentResolver란?  (0) 2019.12.16
인터셉터란?  (0) 2019.12.07
JWT 토큰 기반 인증  (0) 2019.12.06

+ Recent posts