|  개요

- 지난 시간에 한 작업
1. EC2 프리티어 계정 만들기
2. Yum 설치하기 
3. JDK 설치하기

- 이번 시간에 한 작업 
1. EC2에 도커와 레디스 설치하기
2. EC2 인바운드 규칙 설정하기

 

|  절차

1. EC2 도커와 레디스 설치하기

- 이 부분은 별도의 포스팅에 작성해두었는데, 아래에 적어두었다.

https://why-dev.tistory.com/378

 

[Docker/Redis] Ubuntu에 Redis 설치 & Docker로 Redis 실행

| Redis 설치하기 1. Ubuntu에 Redis 설치 sudo apt-get install redis-server 2. 설치 확인 redis-server --version 3. redis가 6379 포트를 쓰는지 확인 // netstat 없으면 net-tools 설치 sudo apt install net-tools // 6379 포트 쓰는지

why-dev.tistory.com

 

2. EC2 인바운드 규칙 설정하기

- 팀원 분께 인바운드와 아웃바운드에 대해 배웠다.

- 인바운드 : 들어오는 포트을 열어두는 것

- 아웃바운드 : 나가는 포트를 열어두는 것

 

(1) EC2 인스턴스로 가기 

- 보안그룹을 어떤걸 사용하는지를 먼저 확인한다.

 

(2) 네트워크 및 보안 > 보안그룹 

- (1)에서 확인한 보안그룹을 체크하고, [인바운드 규칙 편집]을 선택한다.

- 인바운드 규칙 편집 화면에서 0.0.0.0 모든 IP에서 레디스 6379 포트로 접속이 가능하게 하고 규칙을 저장한다.

 

(3) 텔넷을 통해 접속이 가능한지 확인한다.

- 텔넷을 쓰려면 먼저 텔넷을 깔아주어야한다.

- 나는 윈도우를 사용하는데, 윈도우의 경우 텔넷은 아래와 같이 설치할 수 있었다.

https://jsson.tistory.com/44

 

Windows 10 Telnet(텔넷) 서비스 설치, 사용 방법

네트워크 장비 또는 기타 장비들의 관리를 위해 텔넷(Telnet)을 사용할 때가 있는데요, Windows 10에는 기본적으로 설치가 되어 있지 않습니다. 다만, 기본 패키지 내에 포함이 되어 있기 때문에 간단

jsson.tistory.com

* ping : 주소에 대한 접근이 가능한지 확인

* telnet : 특정 ip의 포트로 접근이 가능한지 확인

- cmd를 열어 아래와 같이 작성하면 EC2의 해당 위치로 접속이 가능해진다.

telnet {ip 주소} {port 번호}

ex.
telnet 12.123.145.21 6379

 

(4) 내 로컬 PC에서 작성중이던 프로젝트를 열고 application.properties의 redis 주소를 수정하고 테스트를 해본다.

spring.redis.host={EC2 탄력적 ip주소}
spring.redis.port=6379

'Infra > AWS, Azure' 카테고리의 다른 글

[AWS] AWS EC2에 접속하는 방법 (스크랩)  (0) 2022.12.28
[AWS] EC2 Ubuntu 서버에 JDK 설치하기  (0) 2022.12.05

1. RedisTemlate을 통해 다양한 형태의 데이터 CRUD하기

https://blog.kingbbode.com/25

 

Spring Boot에서 Redis 사용하기

Redis란?Remote Dictionary Server의 약자오픈 소스 소프트웨어휘발성이면서 영속성을 가진 key-value 저장소Redis는 NoSQLNoSQL은 데이터 간의 관계를 정의하지 않고 고정된 스키마를 갖지 않는 새로운 형태의

blog.kingbbode.com

 

2. Json으로 파싱하여 저장, 조회하기

https://velog.io/@kshired/Spring-Redis%EC%97%90%EC%84%9C-%EA%B0%9D%EC%B2%B4-%EC%BA%90%EC%8B%B1%ED%95%98%EA%B8%B0

 

[Spring] Redis에서 객체 그래프를 유지하며 "직접" 캐싱하기

Spring에서 @Cacheable 어노테이션을 이용하면 한 함수에서 같은 인자가 들어왔을 때, return 값을 caching 할 수 있다는 것은 대부분 아는 사실입니다. 하지만 가끔은 로직상에서 캐싱을 해야하는 경우

velog.io

 

|  Redis 설치하기

 

1. Ubuntu에 Redis 설치

sudo apt-get install redis-server

2. 설치 확인

redis-server --version

3. redis가 6379 포트를 쓰는지 확인

// netstat 없으면 net-tools 설치
sudo apt install net-tools

// 6379 포트 쓰는지 확인
netstat -nlpt | grep 6379


tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -
tcp6       0      0 ::1:6379                :::*                    LISTEN      -

4. redis에 접속 후 테스트

// 접속
redis-cli

// 테스트
127.0.0.1:6379> set test1 testvalue
OK

get test1
127.0.0.1:6379> get test1
"testvalue"

 

|  Docker 로 Redis 이미지 생성하기

 

1. Ubuntu에 Docker 설치

sudo docker apt install docker.io

 

2. Docker 로그인 하고 Docker 서비스가 실행되는지 확인하기

// 로그인
sudo docker login -u <아이디>
Password : <패스워드 작성 후 엔터>

// 실행확인
service docker status

 

3. 기존의 Redis-server 끄고, Docker에서 Redis image 생성하기

// 일단 redis-server 끄고
sudo systemctl stop redis-server

// 서비스 꺼진거 확인하고
service --status-all

// docker에서 image 생성하면서 실행
sudo docker run -it --name <이미지명> -p 6379:6379 -d redis

 

4. 컨테이너 확인하기 

sudo docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS    PORTS     NAMES
b037231966f6   redis     "docker-entrypoint.s…"   23 minutes ago   Created             zero-mall-product-redis

 

5. 컨테이너 실행하기

sudo docker start b037231966f6

 

 

[ 참고 ]

서비스 시작/중지 https://vitux.com/how-to-start-stop-or-restart-services-in-ubuntu/

레디스 설치 https://hayden-archive.tistory.com/429

도커에 레디스 이미지 생성 https://icodebroker.tistory.com/9067

컨테이너 리스트 확인 https://codechacha.com/ko/docker-list-containers/

|  개요

- 이번 시간에 한 작업 : 
1. EC2 프리티어 계정 만들기
2. Yum 설치하기 
3. JDK 설치하기

 

1. EC2 프리티어 계정 만들기

- 이 부분은 이전에 우분투로 프리티어 계정을 만들어둔게 있어서 그걸 활용했습니다.

- 프리티어의 경우 하나의 인스턴스를 실행 시켜둘 수 있는데, 하나를 만들어두었고, 탄력적 IP를 설정해두었어요.

 

 

2. EC2 ubuntu에 Yum 설치하기

- 저는 AWS EC2로 우분투 운영체제를 사용합니다.

- 리눅스 계열의 운영체제에서 패캐지를 설치하기 위해서는 RPM 또는 YUM을 사용해야 합니다.

https://velog.io/@jwpark06/Linux-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95-RPM-YUM

 

Linux의 패키지 설치 방법 RPM & YUM이란?

AWS EC2에 설치하기에 관련된 포스팅을 진행하다보니 yum, rpm, yum.repos.d, GPG-KEY 등의 용어들이 사용되는데, 명확히 모르고 쓰고 있었던 것 같아서 한번 정리해볼까 합니다.RPM과 YUM둘 모두 Linux 환경

velog.io

- 따라서 먼저, 아래의 명령어를 통해 Yum을 설치합니다.

sudo apt install yum

 

3. 우분투에서 터미널로 JDK 설치하기

- 이제 리눅스에서 아래의 명령어들을 통해 JDK 11 버전을 설치하고, 잘 설치되었는지 확인합니다.

// 설치하기
$ sudo apt-get install openjdk-11-jdk

// 버전확인
ubuntu@ip-172-31-41-208:~$ java -version
openjdk version "11.0.17" 2022-10-18
OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 11.0.17+8-post-Ubuntu-1ubuntu222.04, mixed mode, sharing)

ubuntu@ip-172-31-41-208:~$ javac --version
javac 11.0.17

 

 

[ 참고 및 출처 ]

https://pinggoopark.tistory.com/14

https://develop-writing.tistory.com/121

https://davelogs.tistory.com/71

|  스프링 부트 Test 중, 아래와 같이 오류가 나타났다.

C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java:33: warning: @Builder will ignore the initializing expression entirely. If you want the initializing expression to serve as default, add @Builder.Default. If it is not supposed to be settable during building, make the field final.
    private List<ProductItem> productItems = new ArrayList();
                              ^
Note: C:\sebinSample\cms\order-api\src\main\java\org\zerobase\cms\order\domain\model\Product.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

- 테스트 코드 

@Test
void addProduct() {

    // given
    Long sellerId = 1L;

    AddProductForm form = makeProductForm("나이키 에어포스", "신발", 3);

    // when
    Product p = productService.addProduct(sellerId, form);

    // then
    Product result = productRepository.findById(p.getId()).get();

    Assertions.assertNotNull(result);
    Assertions.assertEquals(result.getSellerId(), 1L);
    Assertions.assertEquals(result.getName(), "나이키 에어포스");
    Assertions.assertEquals(result.getDescription(), "신발");
    Assertions.assertEquals(result.getProductItems().get(0).getName(), "나이키 에어포스0");
    Assertions.assertEquals(result.getProductItems().get(0).getPrice(), 10000);

}

private static AddProductForm makeProductForm(String name, String description, int itemCount) {
    List<AddProductItemForm> addProductItemForms = new ArrayList<>();
    for (int i = 0; i < itemCount; i++) {
        addProductItemForms.add(makeProductItemForm(null, name + i));
    }
    return AddProductForm.builder()
            .name(name)
            .description(description)
            .addProductItemForms(addProductItemForms)
            .build();
}

> 원인 :

- @OneToMany의 default fetch type이 LazyLoading이기 때문에, Proxy로 id 값만 담은 ProductItem들을 가져오고, 실제 내용은 들어있지 않았기 때문이다.

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited  // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long sellerId;

    private String name;
    private String description;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductItem> productItems = new ArrayList();


    public static Product of(Long sellerId, AddProductForm form) {
        return Product.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .description(form.getDescription())
                .productItems(form.getAddProductItemForms().stream()
                        .map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
                ).build();
    }
}

> 해결 :

이를 해결하기 위해서는 두가지 방법을 사용할 수 있는데

(1) fetch type을 EAGER로 변경한다. --> 이 방법은 그러나 불필요하게 매번 DB 조회 시 모든 데이터를 한 번에 가져오게 함으로 좋지 않다.

(2) JPA의 @EntityGraph와 findWith 함수를 통해 속성을 지정(ex. productItems)할 때, fetch type을 변경시킨다.

EntityGraphType.LOAD attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 default fetch type으로
EntityGraphType.FETCH attributePaths가 지정된 경우 EAGER로, 지정되지 않으면 LAZY로
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    @EntityGraph(attributePaths = {"productItems"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<Product> findWithProductItemsById(Long id);

}

 

[ 출처 ]

부트캠프 수업 내용 정리

|  CascadeType

     
ALL - 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 모두 전파
PERSIST - 상위 엔터티에서 저장을 하면 하위 엔터티도 저장 (영속성 전파) x.persist() 전파
MERGE - 하위 엔터티까지 병합 작업을 지속 ...(?) x.merge() 전파
REMOVE - 하위 엔터티까지 제거 작업을 지속 x.remove() 전파
REFRESH - 하위 엔터티까지 인스턴스 값 새로 고침 (다시 조회) x.refresh() 전파
DETACH - 하위 엔터티까지 엔터티 제거 x.detach() 전파

* persist() 는 리턴값이 없는 insert, merge() 는 리턴값이 없는 update

@Transactional
    public <S extends T> S save(S entity) {
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

* save() 는 리턴값이 있는 insert, update이다.

 

|  예시 코드

- Product (상품) 하위에 옵션으로 들어가는 Item들이 있다고 할 때,

- Product의 List<ProductItem> productItems에 @OneToMany(cascade = CascadeType.ALL)을 달아주어, 상품에 대한 CRUD가 이루어질 때 하위 엔터티인 productItems의 ProductItem까지 영향을 받는다. 

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited  // Entity가 변할 때마다, 변화된 내용을 저장
public class Product extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long sellerId;

    private String name;
    private String description;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductItem> productItems = new ArrayList();


    public static Product of(Long sellerId, AddProductForm form) {
        return Product.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .description(form.getDescription())
                .productItems(form.getAddProductItemForms().stream()
                        .map(p -> ProductItem.of(sellerId, p)).collect(Collectors.toList())
                ).build();
    }
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@AuditOverride(forClass = BaseEntity.class)
@Audited
public class ProductItem extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long sellerId;

    @Audited
    private String name;

    @Audited
    private Integer price;

    private Integer count;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private Product product;

    public static ProductItem of(Long sellerId, AddProductItemForm form) {
        return ProductItem.builder()
                .sellerId(sellerId)
                .name(form.getName())
                .price(form.getPrice())
                .count(form.getCount())
                .build();
    }
}

>> 이슈! : 하위에서 삭제시, 상위로 CasecadeType.ALL이 전파되었다. (전부 다 삭제됨)

>>> 수업에서 해결할 때에는, 하위의 CascadeType.ALL을 지웠더니 해결되었다.

 

[ 출처 및 참조 ]

부트캠프 수업 내용 정리

https://data-make.tistory.com/668

 

[JPA] Spring JPA CascadeType 종류

JPA Cascade Types Spring JPA CascadeType 종류 javax.persistence.CascadeType JPA Cascade Type ALL PERSIST MERGE REMOVE REFRESH DETACH CascadeType.ALL 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 @Entity public class Person { @Id @Gen

data-make.tistory.com

https://gimmesome.tistory.com/207

 

[JPA] save와 persist차이 (save, persist, merge개념)

persist()는 리턴값이 없는 insert다. merge()는 리턴값이 없는 update다. save()는 리턴값이 있는 insert, update다. save 메소드를 호출하면.... entityInformation에서 새로운 entity이면 persist()를 그게 아니면 merge()

gimmesome.tistory.com

https://umanking.github.io/2019/04/12/jpa-persist-merge/

https://2jinishappy.tistory.com/337

 

좋은 Pull Request를 만드는 방법과 PR Template 구성

https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository Creating a pull request template for your repository - GitHub Docs For more information, see "About iss

2jinishappy.tistory.com

 

|  Redis insight란?

- Redis GUI이다. 사용자 편의를 위한 인터페이스임.

- 다운로드 링크 : https://redis.com/redis-enterprise/redis-insight/

 

RedisInsight | The Best Redis GUI

RedisInsight provides an intuitive and efficient graphical interface for Redis, allowing you to interact with your databases and manage your data.

redis.com

 

|  사용 사례

- Docker를 통해서 Redis를 pull, run 한 후,

- Redis insight를 통해서 database를 입력하여 들어왔다.

- 지금 어느정도의 공간을, 어떻게 사용하고 있는지를 한눈에 볼 수 있다.

|  Cache란?

용어 What? why? How?
Cache 임시로 데이터를 저장하는 공간 성능 향상 - Look aside Cache, Write Back
- Memcahced, Redis API

 

1. Cache의 배경과 목적

- 파레토의 법칙에 따르면, 80%의 결과는 20%의 원인에 의해 발생한다.

- 다시말해, 사람들이 자주 쓰는 데이터는 정해져 있고 이를 캐싱해서 저장하면 DB에 접근할 필요가 없어진다.

- 서비스를 런칭하고 사용자가 늘어나면 그만큼 DB 작업량도 늘어나기 마련이라 한다.

- 이럴 때 캐시를 사용한다면, 성능을 개선할 수 있다.

 

2.  Cache의 사용 방식

- 캐시는 조회 또는 쓰기를 할 때에 사용될 수 있다.

Look Aside Cache (Lazy Loading) 캐시를 한 번 접근하여 데이터의 존재 유무에 따라 바로 반환 또는 DB (or API) 호출
Write Back 쓰기(Insert) 작업이 많을 때 쿼리를 모아서 배치(Batch)처리

(1) Look aside Cache

- 한 번 조회한 데이터를 캐시에 저장 후, 동일한 데이터를 조회할 때,

  DB에 direct로 접근하지 않고, Look aside(주의를 돌려) 캐시의 내용을 확인한다.

- Process

  - 사용자가 웹서버에 요청을 보낸다.

  - 웹서버는 Cache를 먼저 조회한다.

  - Cache에 데이터가 있다면, 사용자에게 해당 데이터를 반환(Cache Hit)하고,

    Cache에 데이터가 없다면, DB에서 데이터를 조회한 후 해당 데이터를 반환(Cache Miss)한다.

(2) Write Back

- 캐시 안에 일시적으로 저장한 후 한 번에 모아 배치 처리를 하는 것을 말한다.

- 하지만 Write Back을 사용하면 캐시 안의 데이터가 중간에 유실될 수 있기 때문에 사용에 주의가 필요하다.

(3) 그 외에도 Write Through 등등이 있다.

 

|  Redis

REDIS : Remote (외부에 있는) Dictionary (Key-Value형태) Server (서버)

 

1. Redis란?

  한줄 요약 특징
Redis Key-value 형태의 값을 저장할 수 있는 In-Memory 저장소 - Key-Value 형식의 NoSql
- In-memory
- 다양한 데이터 타입 제공
- 주로 캐시로 사용
- 싱글 쓰레드
- Atomic

- Redis는 인메모리 기반의 data structure store로, db/cache/message broker, streaming engine으로 사용될 수 있다.
  * 많은 개발자들은 Redis를 Store가 아닌 Cache라고 분류한다.
  * Redis는 지속성을 보장하기 위해 데이터를 In-Memory 곧, Disk에 저장할 수 있다.

  * 단, In-memory지만 영구적으로 저장할 수 있기도 하다.

- Redis는 NoSQL로 분류되며, Key-Value 형태로 데이터를 저장한다.
- Redis는 문자열, 해쉬, 리스트, Set, sorted Set 형태의 다양한 자료구조를 지원한다.
- Redis는 기본적으로 Single-Threaded하며, 자료구조는 Atomic 한 성질(=원자성, All or nothing)을 가지고 있어

  race condition이 일어나는 것일 예방할 수 있다는 장점을 가지고 있다.

 

2. Redis의 다양한 모드

종류 서버 특징
Single 단일서버 HA미지원
Sentinel 여러서버 Master-slave* 방식, HA지원*
Cluster

* HA(High Availability) : 서버 하나가 죽더라도 나머지 서버들이 서비스를 계속 운영할 수 있도록 하는 전략

* Master-Slave

  - Master : 한 서버에는 하나의 Master노드가 있다.

  -Slave    : Master노드에 대한 복사본

- Cluster 모드에서는, 여러 서버가 있다고 전제할 때, 각 서버의 Master의 복제본은 다른 서버의 Slave에 저장한다.

  이렇게 할 경우, 서버가 다 죽고 하나만 남는다고 하더라도 운영에 지장이 생기지 않게된다.

  1번서버 2번서버 3번서버
Master Master1 Master2 Master3
Slave Master2 복제본 Master1 복제본 Master1 복제본
Slave Master3 복제본 Master3 복제본 Master2 복제본

 

|  Redis 설치하기

** 참고사항 : embeded-redis를 사용할 경우, 아래와 같이 별도로 redis를 설치하지 않고도,
                     스프링 build.gradle에 라이브러리를 추가하는 것만으로도 redis를 사용할 수 있다.

 

1. OS에 맞게 Redis를 설치한다.

https://redis.io/docs/getting-started/

- 윈도우의 경우 리눅스 환경을 만든 후에야 Redis를 설치할 수 있다.

- 윈도우에서 설치할 수 있는 다른 방법이 있는데 아래 사이트를 가면 된다.

https://github.com/microsoftarchive/redis/releases

* 자세한 설치과정에 대한 포스팅

 

[REDIS] 📚 Window10 환경에 Redis 설치 & 설정

Redis 윈도우 설치 Redis 다운로드 페이지로 이동하여 설치 프로그램을 다운로드하고 설치를 진행한다. Releases · microsoftarchive/redis Redis is an in-memory database that persists on disk. The data model is key-value, but

inpa.tistory.com

 

2. redis-cli.exe를 실행한 후, Redis 명령어를 입력해본다.

* Redis gate에 가면 더 자세한 명령어를 확인할 수 있다.

  http://redisgate.kr/redis/introduction/redis_intro.php

• $> set myKey myValue
• $> OK


• $> get myKey
• $> myValue


• $> del myKey
• $> myValue


• $> get myKey

• $> (nil) 

• $> keys*             <-- 모든 데이터 확인 (실제 운영 시에는 사용X)

• $> set key value <-- redis-server.exe 서버 종료

 

|  스프링에서 Redis 사용하기

 

1. 시작하기

(1) Build.gradle에 Redis 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

(2) application.yml에 redis ip주소 및 port번호 입력

redis:
  host: localhost
  port: 6379

(3) @SpringBootApplication 클래스에 @EnableCaching을 추가

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class BaedanguemApplication {

    public static void main(String[] args) {
        SpringApplication.run(BaedanguemApplication.class, args);

    }

}

 

2. RedisConnectionFactory 및 CacheManager Bean 생성

(1) RedisConnectionFactory 빈 생성

- 사용하는 모드에 따라, Configuration인스턴스를 생성한 후, host, port, password 등을 setting한다.

- 이후 LettuceConnectionFactory에 해당 configuration을 넣어 반환하면 빈 생성이 완료된다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){

        // Single 모드
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();

        // Cluster 모드
        // RedisClusterConfiguration conf = new RedisClusterConfiguration();

        conf.setHostName(host);
        conf.setPort(port);

        return new LettuceConnectionFactory(conf);
    }

}

* C:\Program Files\Redis 의 redis.windows-service.conf 파일에서 port, required password 등을 설정할 수 있다.

 

(2) CacheManager 빈 생성

- RedisConnectionFactory를 매개변수로 담은 메소드를 생성한다. (반환 값은 CacheManager)

- RedisCacheConfiguration.defaultCacheConfig()key, value에 대한 Serialization 방식을 설정한다.

!! Redis에 data 또는 Object를 저장하기 위해 직렬화(Serialization)를 통해 data, object를 Byte 단위로 변경한다.

  * Redis는 Java 시스템 외부의 캐시 서버이기 때문에, 외부에 데이터를 저장하기 위해서는 직렬화가 필요하다.

  * 마찬가지로 Redis에서 데이터를 가져와 data, object로 변환할 때에 역직렬화를 사용한다.

- RedisCacheManager.RedisCacheManagerBuilder를 통해

  매개변수의 RedisConnectionFactory와, 바로 위에서 작성한 RedisCacheConfiguration을 담아 반환한다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){

        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(conf)
                .build();
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){

        // Single 모드
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();

        // Cluster 모드
        // RedisClusterConfiguration conf = new RedisClusterConfiguration();

        conf.setHostName(host);
        conf.setPort(port);

        return new LettuceConnectionFactory(conf);
    }

}

 

3. 조회 요청 시 캐시 저장하기

(1) 조회 요청시 호출되는 메소드에 @Cacheable(key = "", value = "") 추가하기

@Service
class XXXService{

	// 사전 고려사항 : 
	// (1) 자주 요청되는 데이터인가? 그렇다면 캐싱
	// (2) 변경이 잦은 데이터인가?   그렇다면 캐싱x

	// redis server의 key-value와 다른 개념
	@Cacheable(key = "#companyName", value = "finance")
	public ScrapedResult getDividendByCompanyName(String companyName) {
		// 메소드 내용
	}
    
}

ㄴ value 값을 일일히 입력하지 않고, 상수화하여 사용할 수도 있다.

package com.example.baedanguem.model.constants;

public class CacheKey {
    
    public static final String KEY_FINANCE = "finance";
    
}

 

(2) Http 요청을 보낸 후 결과 확인하기

- 수업에서는 Serialization 관련하여 InvalidDefinitionException 오류가 발생했다.

- 원인 : LocalDateTime 타입의 데이터를 캐시에 저장하기 위해 직렬화 하는 과정에서 타입 미스매치 발생

   - 3.에서 직/역직렬화를 StringRedisSerializer가 담당하고 있었기 때문에 LocalDateTime은 직렬화할 수 없다.

- 해결 방법 : 아래 확인

 

(3) 웹서버<->캐시 data 직렬화, 역직렬화  과정에서 타입 미스매치 시

- 수업에서는 Json 타입의 데이터를 송수신 하기 때문에 @JsonSerialize와 @JsonDeserialize를 통해

  직렬화/역직렬화 방식을 지정해주었다.

@Data
@Builder
public class Dividend {

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime date;

    private String dividend;

}

>> 이 상태에서, 프로그램을 실행한 후, 컨트롤러를 통해 최초로 데이터를 조회할 경우, 정상적으로 결과값을 받을 수 있다.

>> 그리고 redis-cli.exe에서 keys * 를 입력해 캐싱이 잘 되었는지 확인하면 결과는 아래처럼 나타난다.

127.0.0.1:6379> keys *
1) "finance::3M Company"
127.0.0.1:6379> get "finance::3M Company"
"{\"@class\":\"com.example.baedanguem.model.ScrapedResult\",\"company\":{\"@class\":\"com.example.baedanguem.model.Company\",\"ticker\":\"MMM\",\"name\":\"3M Company\"},\"dividends\":[\"java.util.ArrayList\",[{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2016,11,16,0,0],\"dividend\":\"1.11\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,2,15,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,5,17,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,8,23,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,11,22,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,2,15,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,5,17,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,8,23,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,11,21,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,2,14,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,5,23,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,8,15,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,11,21,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,2,13,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,5,21,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,8,21,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,11,19,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,2,11,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,5,20,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,8,20,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,11,18,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,2,17,0,0],\"dividend\":\"1.49\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,5,19,0,0],\"dividend\":\"1.49\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,8,19,0,0],\"dividend\":\"1.49\"}]]}"

>> 그런데, 이 상태에서 다시 컨트롤러를 통해 동일한 조회 요청을 보내면

     이번엔 아래와 같이 Deserialization을 하려는 Object가 생성자가 없는 Object라는 오류가 나타난다.

     - 원인 : Object에 생성자가 없기 때문에

     - 의문 :  왜 처음에는 오류가 안 났나? == 처음에는 캐시에서 내용을 찾아오지 않고, DB에서 찾아와서.

     - 해결 : Object에 @NoArgsConstructor나 @AllArgsConstructor를 추가한다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.baedanguem.model.Company` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"com.example.baedanguem.model.ScrapedResult","company":{"@class":"com.example.baedanguem.model.Company","ticker":"MMM","name":"3M Company"},"dividends":["java.util.ArrayList",[{"@class":"com.example.baedanguem.model.Dividend","date":[2016,11,16,0,0],"dividend":"1.11"},{"@class":"com.example.baedanguem.model.Dividend","date":[2017,2,15,0,0],"dividend":"1.175"},{"@class":"com.example.baedanguem.model.Dividend","date":[2017,5,17,0,0],"dividend":"1.175"},{"@class":"com.example.baedanguem.m"[truncated 1906 bytes]; line: 1, column: 115] (through reference chain: com.example.baedanguem.model.ScrapedResult["company"])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.12.5.jar:2.12.5]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764) ~[jackson-databind-2.12.5.jar:2.12.5]

 

(4)  추가적인 내용

- 스프링 프로그램을 종료하더라도, 캐시 서버에는 데이터가 남아있다.

- @Cacheable 메소드 안에 log를 남길 경우, 처음 DB에서 데이터를 조회할 때 콘솔에 로그가 남지만,

  캐시에서 데이터를 조회할 때에는 콘솔에 log가 남지 않는다.

 

4. 레디스 캐시 삭제

- 데이터가 업데이트 되었음에도 캐시에 이전 데이터가 남아다면, 이전 조회기록을 가져오게 된다.

- 따라서, 해당 캐시의 내용을 업데이트하거나, 해당 내용을 삭제하는 것이 필요하다.

- 더불어, 캐시 공간 또한 한계가 있기 때문에 오래된 캐시 데이터는 삭제하는 것이 필요하다.

* 수업에서는, 매일 00시에 데이터를 받아 업데이트를 진행하기 때문에, 스케줄러 메소드에 작업을 했다.

 

(1) @CacheEvict를 사용하여 특정 value 의 데이터를 삭제한다.

- 앞서서 캐시한 조회 메소드에 @Cacheable(key = "#companyName", value = "finance") 설정을 했었다.

- @CacheEvict의 value에 설정했던 "finance"를 넣어주고, allEntries = true 를 하면 "finance"로 캐시된 모든 데이터를 삭제한다.

@CacheEvict(value = CacheKey.KEY_FINANCE, allEntries = true)
@Scheduled(cron = "${scheduler.scrap.yahoo}")
public void yahooFinanceScheduling() {
   // 내용  
}

- 만약, 특정 데이터만 삭제하고 싶다면, key값에 해당 데이터의 key값을 넣어주면 된다.

@CacheEvict(value = CacheKey.KEY_FINANCE, key = "someCompany")
@Scheduled(cron = "${scheduler.scrap.yahoo}")
public void yahooFinanceScheduling() {
   // 내용  
}

 

(2) @CacheEvict를 붙인 스케줄러에 @EnableCaching을 추가한다.

- 스케줄러는 Main 메소드와 다른 쓰레드를 사용한다.

- 스케줄러가 동작할 때마다, @EnableCaching이 이루어질 수 있도록 추가한다.

@Component
@AllArgsConstructor
@EnableCaching
@Slf4j
public class ScraperScheduler {

    @CacheEvict(value = CacheKey.KEY_FINANCE, allEntries = true)
    @Scheduled(cron = "${scheduler.scrap.yahoo}")
    public void yahooFinanceScheduling() {
       // 내용  
    }
    
}

 

(3) TTL (Time to Live) 설정하기

- TTL : 데이터의 유효기간

- 앞에서 CacheConfig작성시 RedisCacheConfiguration을 작성했던 부분에 TTL 기간을 추가한다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){

        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofDays(30)); // TTL 시간 지정 

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(conf)
                .build();
    }

}

 

 

[ 참고 및 출처 ]

부트캠프 내용 정리

Redis란?

https://steady-coding.tistory.com/586

https://www.baeldung.com/spring-boot-redis-cache

Redis 윈도우 10 환경에서 설치 후 실행하기

https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-Window10-%ED%99%98%EA%B2%BD%EC%97%90-Redis-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0

 

|  관련 용어

용어 What? How?
Lazy Loading 사용자가 보지 않는 리소스는 차후에 로딩하는 기술 - 프론트 : 무한 스크롤, placeholder 등
- 백엔드 : JPA의 지연 로딩

 

|  JPA의 Lazy Loading 

- 개발자는 JPA를 통해 프록시를 만들 수 있다.

- 프록시란, 가짜 객체를 말하는 것으로, 실제 엔터티에 대한 참조값을 가진다.

- 하나의 엔터티가 다른 엔터티와 연관관계를 맺고 있을 때 (oneToMany) , 

  연관된 객체들을 처음부터 DB에서 조회하지 않고, 실제 사용하는 시점에 DB에서 조회하면 속도를 향상시킬 수 있다.

- JPA에서는 프록시가 참조하는 객체들의 데이터 조회 시점을 정하는 타입을 Fetch Type이라 하는데, 

즉시로딩 - 한 객체를 조회할 때, 참조 객체들까지 전부 읽어온다.(EAGER),
지연로딩 - 한 객체를 조회할 때, 참조 객체들은 무시하고 해당 객체의 엔터티 데이터만 가져온다.(LAZY)

- 그 타입 중에서도 지연로딩(LAZY)이 바로 Lazy Loading이다.

 

|  General한 Lazy Loading 용어 의미

- Lazy Loading은 JPA 뿐만 아니라, 프론트에서도 사용하는 용어이다. 

- 프론트에서는 이미지나 동영상 등의 리소스들을 전부 다 올리지 않고 사용자가 필요로 할 때에만 로딩하기도 하는데,

- 이 또한 페이지의 로딩이 너무 느려지는 현상을 방지하기 위해서이다.

- 결국 정리하자면, 지연 로딩은 사용자가 당장 쓰지 않는 불필요한 데이터의 로딩을 지연시키겠다는 의미이다.

 

 

[ 참고 및 출처 ]

Lazy Loading,

https://scarlett-dev.gitbook.io/all/it/undefined-1

https://programmer-chocho.tistory.com/81

https://victorydntmd.tistory.com/210

https://velog.io/@bread_dd/JPA%EB%8A%94-%EC%99%9C-%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

+ Recent posts