Redis

  1. Redis & Cache
  • Cache : 나중에 요청할 결과를 미리 저장해둔 후 빠르게 서비스 해주는 것. 미리 결과를 저장하고 나중에 요청이 오면 그 요청에 대해서 DB 또는 API를 참조하지 않고 Cache에 접근해서 요청을 처리한다.

[progress]

1) Client로부터 요청을 받는다.

2) Cache에서 이미 들어왔던 작업인지 확인한 후 만약 참이면 Cache 내용을 리턴한다.

3) 거짓이라면 DB or Server에 접근해서 작업한다.

4) DB를 접근했기 때문에 한번 들어온 내용은 Cache에 저장이 되고 따라서 다시 Cache와 작업한다.

  • Redis(Remote Dictionary[key,value] Server, 원격 사전 서버)

[properties]

1) In-Memory database로 read/write가 빠른 NoSQL이다.

2) Persistence(영속성)를 지원하므로 서버가 꺼지더라도 다시 data를 불러들일 수 있다.

3) key-value 형태로 data를 저장하기 때문에 별도의 쿼리 없이 key로 value를 찾을 수 있다.

4) Data Collection : String, List, Set, Sorted Set, hash

5) Memory 기반의 DB이므로 get, put을 이용해서 데이터를 넣고 빼는 것이 가능하다.

💡 Redis Collection(자료구조)와 명령어

 

1) String(key-value : One-To-One Mapping)

  • get : key에 해당하는 value 리턴
  • set : key에 value 저장
  • del : key 삭제

2) List(순서가 있는 String 묶음으로 array형식의 데이터 구조이다. 자바의 linked list와 유사하다. 처음과 끝에 데이터를 삽입/삭제는 빠르지만, 중간에 데이터를 삽입하는 것이 어렵다.)

  • lpush <key><value> : left, 첫번째(index:0)에 데이터 삽입
  • rpush <key><value> : right, 마지막(index:-1)에 데이터 삽입
  • lpop <key><value> : left, 첫번째(index:0)에 데이터 삭제
  • rpop <key><value> : right, 마지막(index:0)에 데이터 삭제
  • lrange <key><s><e> : s부터 e index의 데이터 반환

3) Sorted Set(Set과 마찬가지로 순서가 없는 String의 묶음. 다른점은 score를 통해 순위를 매겨 오름차순으로 정렬이 가능하다. 만약, score값이 같으면 value로 정렬한다. 여기서 set의 value는 member이다. value는 중복이 불가능하지만, score 값은 중복이 가능하다.)

  • zadd <key><score><member> : key에 score와 member 추가
  • zcard <key> : member count 조회
  • zcard <key><s><e> : s부터 e까지 index의 member 반환
  • zrangebyscore <key><min><max> : min부터 max까지 해당하는 score 값을 갖는 member 반환
  • zrem <key><member> : member 삭제

4) Hash(key내부에 key-value가 존재하는 자료구조이다. key의 Filed는 subkey로 갯수에 제한이 없다. 여러가지 Object type을 저장할 수 있다.)

 

  • hset <key><field><value><field><value>.. : 하나의 key에 여러 field-value 저장
  • hget <key><field> : key와 field에 해당하는 value 리턴
  • hdel <key><field> : field 삭제
  • hlen <key> : field count 리턴
  • hgetAll <key> : 모든 field-value 쌍 반환
  • hkeys <key> : 모든 field 리턴
  • hvals <key> : 모든 value 리턴

[Reference]

 

[Redis] 레디스와 캐시 그리고 데이터구조

이번에 회사에서 프로젝트를 맡으면서 redis를 접할기회가 생겼다. 근데 익숙하지 않은 데이터베이스라서 한번 정리해보려고 한다. 그리고 시작하기 전에 레디스를 살펴볼 gui툴로 medis도 설치했

pearlluck.tistory.com

2.pub/sub Model

<aside> 💡 Kafka VS Redis Cluster에서 Pub/Sub 동작원리

</aside>

*차이점 요약: pub/sub을 다루는데 있어서 kafka가 훨씬 무겁다.

 

1) pub/sub 모델에 대한 이해

2) Kafka pub/sub model

kafka 는 producer/consumer(publisher/subscriber)가 있는데 producer가 Topic에 이벤트를 보내고 이 이벤트는 Topic의 각 Partition에 분산되어 저장된다. Topic을 구독하고 있는 Consumer group 내의 Consumer는 각각 1개 이상의 partition으로부터 이벤트를 가져온다. 만약 partition 개수보다 consumer개수가 많다면 아무 일도 하지 않는 consumer가 생기기 때문에 항상 partition의 수를 consumer의 수 이상으로 해주는게 좋다.

3) Redis pub/sub model

Redis의 pubsub은 그룹이라는 개념이 존재하지 않고 각 subscriber가 channel을 구독하고 있다. 여기서 kafka와의 가장 큰 차이점으로 channel은 이벤트를 저장하지 않는다. 이벤트가 도착했을때 채널의 subscriber가 없다면 이벤트는 증발한다. 또한 응답을 받지 못하기 때문에 완벽한 이벤트의 성공을 보장하지는 못한다.

 

Kafka는 redis보다 더 다양한 기능이 있고 세밀하게 성능 및 기능을 조정할 수 있다. 그러나 Redis도 kafka처럼 사용할 수 있으므로 Event queue가 무겁고 core하게 pub/sub 기능이 필요하다면 kafka를 추천하고 단순한 이벤트큐로 가볍게 사용하려고 한다면 redis를 권장한다.

 

💡 Back to Redis(Kafka는 이후에 다뤄보도록 하겠다.)

 

1) server.pubsub_channels → server측에서 channel을 저장하는 구조

 

pubsub_channel는 채널을 저장하는 dict 구조체를 가리킨다. dict 구조체는 여러개의 dictEntry를 가지고 있고 각 Entry는 key:robj channel, value:list 타입인데 list는 linked list로 이루어져 있다.

public 동작 과정은 다음과 같다. PUBLISH channel_name message → dict(hash table)에서 channel을 찾고 그 value에 있는 linked list를 돌면서 client들에게 메세지를 보낸다.

 

2) server.pubsub_patterns → server 측에서 pattern을 저장하는 구조

 

PUBLISH channel_name message → 일단 pubsub_channels 에서 처럼 channel 명으로 클라이언트에 메시지를 보낸 다음  를 돌며 패턴에 맞는 client에게 메시지를 보낸다. (pattern을 보면 robj 라는 단어가 붙어있는데 저건 redis object라고 생각하면 된다. 참고로 redis는 C언어로 개발되어있다.)

  1. STOMP & Redis

[INTRO]

💡 Why using Redis?

 

1) 채팅방의 메인 저장소가 없어 server의 memory에 적재된 채팅방은 server을 재시작 할때마다 초기화된다. 따라서 DB를 이용하거나 저장소가 필요한데 Redis를 저장소로 사용할 수 있다.

2) STOMP에서 지원해주는 pub/sub 기능을 사용하면 pub/sub이 발생한 server 내에서만 메세지를 주고받는 것이 가능하다. 즉, subscribe 대상인 topic(채팅방)이 생성된 server안에서만 유효하므로 다른 서버로 접속한 client는 해당 채팅방이 보이지 않고, 구독도 불가능하다. 이는 Redis에서 지원하는 pub/sub을 이용해서 해결할 수 있다.

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 39일차  (0) 2022.10.28
항해 99 38일차  (1) 2022.10.27
항해 99 37일차  (0) 2022.10.26
항해 99 36일차  (0) 2022.10.25
항해 99 35일차  (0) 2022.10.24

WebSocket

Reference

26. WebSocket Support

 

26. WebSocket Support

This part of the reference documentation covers Spring Framework’s support for WebSocket-style messaging in web applications including use of STOMP as an application level WebSocket sub-protocol. Section 26.1, “Introduction” establishes a frame of m

docs.spring.io

[Spring Boot] WebSocket과 채팅 (3) - STOMP

 

[Spring Boot] WebSocket과 채팅 (3) - STOMP

[Spring Boot] WebSocket과 채팅 (2) - SockJS [Spring Boot] WebSocket과 채팅 (1) 일전에 WebSocket(웹소켓)과 SockJS를 사용해 Spring 프레임워크 환경에서 간단한 하나의 채팅방을 구현해본 적이 있다. [Sprin..

dev-gorany.tistory.com

Spring websocket chatting server(1) - basic websocket server

 

웹소켓(websocket)으로 채팅서버 만들기

웹소켓(websocket)으로 채팅서버 만들기

daddyprogrammer.org

https://www.youtube.com/watch?v=rvss-_t6gzg


26.1 Introduction

💡 A proper introduction to the WebSocket protocol is beyond the scope of this document. At a minimum however it’s important to understand that HTTP is used only for the initial handshake, which relies on a mechanism built into HTTP to request a protocol upgrade (or in this case a protocol switch) to which the server can respond with HTTP status 101 (switching protocols) if it agrees handshaking. Assuming the handshake succeeds the TCP socket underlying the HTTP upgrade request remains open and both client and server can use it to send messages to each other.

 

26.1.2 A Messaging Architecture

💡 By contrast a WebSocket application may use a single URL only for the initial HTTP handshake. All messages thereafter share and flow on the same TCP connection.

 

26.1.3 Sub-Protocol Support in WebSocket

💡 Unlike HTTP, which is an application-level protocol, in the WebSocket protocol there is simply not enough information in an incoming message for a framework or container to know how to route it or process it. Therefore WebSocket is arguably too low level for anything but a very trivial application. For this reason the WebSocket RFC defines the use of sub-protocols. The use of a sub-protocol is not required, but even if not used, applications will still need to choose a message format that both the client and server can understand. STOMP-a standard messaging protocol is widely supported and well suited for use over WebSocket and over the web.

 

26.1.4 Should I use WebSocket?

💡 It is the combination of both low latency and high frequency of messages that can make the use of the WebSocket protocol critical. The Spring Framework allows @Controller and @RestControllerclasses to have both HTTP request handling and WebSocket message handling methods.

 


26.2 WebSocket API

WebSocketHandler

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

*//implement WebSocketHandler or more likely extending either
//TextWebSocketHandler or BinaryWebSocketHandler*

public class MyHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // ...
    }

}

WebSocketConfig

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        
				*//mapping the WebSocket handler to a specific URL*
				registry.addHandler(myHandler(), "/myHandler");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}

WebSocket Handshake

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

}

💡 The easiest way to customize the initial HTTP WebSocket handshake request is through a HandshakeInterceptor, which exposes "before" and "after" the handshake methods. Such an interceptor can be used to preclude the handshake or to make any attributes available to the WebSocketSession.

 

26.2.6 Configuring allowed origins

💡 The default behavior for WebSocket and SockJS is to accept only same origin  requests. It is also possible to allow all or a specified list of origins. The 3 possible behaviors are: 1) Allow only same origin requests (default) 2) Allow a specified list of origins: each provided allowed origin must start with http:// or https:// 3) Allow all origins: to enable this mode, you should provide ’’ as the allowed origin value.*

 


26.3 SockJS Fallback

26.3.2 Enable SockJS

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").withSockJS();
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}

26.3.6 SockJS and CORS

💡 If you allow cross-origin requests the SockJS protocol uses CORS for cross-domain support in the XHR streaming and polling transports. Therefore CORS headers are added automatically unless the presence of CORS headers in the response is detected.

 


26.4 STOMP

💡 The WebSocket protocol defines two types of messages, text and binary, but their content is undefined. The use of a sub-protocol is optional but either way client and server will need to agree on some protocol that defines message content. STOMP can be used over any reliable 2-way streaming network protocol such as TCP and WebSocket. Although STOMP is a text-oriented protocol, the payload of messages can be either text or binary*.*

 

 

STOMP Protocol)

[structure of STOMP frame]
관례상 메세지를 교환할 때 “/topic/..”은 pub-sub이 one-to-many일 때, “/queue/’는 one-to-one일 때 사용한다.
ClientA가 5번 채팅방에 대해 SUBSCRIBE(구독)을 하는 예시)
ClientB에서 채팅 메세지를 보내는 예시) 서버는 내용을 기반(chatRoomId)으로 메세지를 전송할 broker에 전달한다. (topic을 sub로 보아도 될 것 같다)
STOMP server는 모든 구독자들에게 메세지를 broadcast 하기 위해 Message command 사용이 가능하다.

💡 Clients can use the SEND or SUBSCRIBE commands to send or subscribe for messages along with a "destination" header that describes what the message is about and who should receive it. This enables a simple publish-subscribe mechanism that can be used to send messages through the broker to other connected clients or to send messages to the server to request that some work be performed.

 

[문서해석]

클라이언트는 메세지의 내용과 누가 해당 메세지를 받을 것인가에 대한 정보를 알려주기 위해 SENDSUBSCRIBE 커맨드 사용이 가능하다. 이는 pub-sub 메커니즘을 사용하여 브로커를 통해 다른 클라이언트에게 메세지를 보내거나 수행되어야 할 기능들을 서버에 요청하기 위한 메세지 송신을 가능하게 한다.

💡 Publisher-Subscriber Model이란?

 

  • Pub/Sub 구조는 비동기식(async) 메세징 패턴이다.
  • Pub과 Sub은 메세지를 특정 수신자에게 다이렉트로 전달하는 시스템이 아니다. Pub은 메세지를 topic을 통해서 카테고리화하고 Sub은 그 topic을 구독함으로써 메세지를 읽어올 수 있다. Publisher은 topic에 대한 정보만 알고 있고, Sub도 topic만 바라보기 때문에 Pub과 Sub은 서로를 모르는 상태이다.
  • Pub는 Sub에 대한 정보를 몰라도 일단 메세지를 Broker(Channel)에 보내놓는다. 이 때 메세지에 맞는 Topic으로 보내놓으면, 해당 Topic을 구독중인 Sub에게만 메세지가 가게 된다.

26.4.4 Flow of Messages

-default broker 사용

(RabbitMQ와 같이 external borker을 사용하는 케이스는 공식 문서 참고)

SimpleBroker은 SUBSCRIBE 중인 다른 클라이언트들에게 메세지를 보낸다. 또한 SimpleBroker은 클라이언트의 SUBSCRIBE 정보를 자체적으로 메모리에 유지한다.

  • clientInboundChannel : WebSocket Client로부터 들어오는 요청을 전달하며, 클라이언트로부터 받은 메세지를 전달한다.
  • clientOutboundChannel : WebSocket Client로 Server의 메세지를 내보내며, 클라이언트에게 메세지를 전달한다.
  • brokerChannel : Server 내부에서 사용하는 채널이며, 이를 통해 SimpleAnnotationMethod는 SimpleBroker의 존재를 직접 알지 못해도 메세지를 전달할 수 있다. 서버의 어플리케이션 코드 내에서 broker에게 메세지를 전달한다.
  • Message : headers와 payload를 포함하는 메세지의 표현
  • MessageHandler : Message 처리에 대한 계약
  • SimpleAnnotationMethod : @MessageMapping 등 Client의 SEND를 받아서 처리한다.
  • SimpleBroker : Client의 정보를 메모리 상에 들고 있으며, Client로 메세지를 보낸다.

[과정]

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJs();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app"); *//For convenience, will use "/pub" later*
        registry.enableSimpleBroker("/topic"); *//For convenience, will use "/sub" later*
    }

}

@Controller
public class GreetingController {

		//same as "/pub/greeting"
    @MessageMapping("/greeting") {
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }

}

💡 “/portfolio” is the HTTP URL for the endpoint to which a WebSocket (or SockJS) client will need to connect to for the WebSocket handshake. STOMP messages whose destination header begins with “/app”are routed to @MessageMapping methods in @Controller classes. Use the built-in, message broker for subscriptions and broadcasting; Route messages whose destination header begins with "/topic" or "/queue" to the broker.

 

[문서 해석]

“/portfolio”는 websocket과의 handshake을 위해 연결되어야 할 endpoint를 의미한다. “/app”으로 destination 헤더가 시작되는 STOMP 메세지들은 컨트롤러에서 @MessageMapping(SEND)로 경로한다. “/topic” 이나 “/queue”로 destination 헤더가 시작되면 브로커로 바로 향하게 된다(SUBSCRIBE).


  1. client가 http://localhost:8080/portfolio”로 연결을 시도하고 webSocket 연결이 성공하면, STOMP frame이 작동되기 시작한다.
  2. 클라이언트가 SUBSCRIBE command를 “/topic/greeting” 이라는 destination 헤더를 보내면 우선 STOMP frame으로 decoding된다. decoding된 내용은 Spring 메세지 형태로 전환되며 이후 “clientInboundChannel”로 보내져 클라이언트의 구독 정보를 저장하고 있는 message broker로 라우팅된다.
  3. 클라이언트는 SEND command를 “/app/greeting”으로 보낸다. “/app” prefix는 어노테이트 된 컨트롤러로 라우팅 될 수 있게 해준다. 이후 “/app” prefix는 잘려지고, 남은 “/greeting” 부분은 컨트롤러에 있는 @MessageMapping와 매핑된다.(걍 pub으로 요청되었다가 나갈 때는 sub 붙여서 나가는거임). STOMP가 핸들러의 역할을 대신 해주므로 핸들러는 없어도 된다.
  4. 컨트롤러에서 반환된 값은 payload와 default destination headerd인 “/topic/greeting”을 담은 스프링 메세지로 전환된다. (prefix였던 “/app”은 잘려나가고 “/topic”으로 대체되어 default destination headerd인 “/topic/greeting”이 된다.)
  5. 메세지는 brokerChannel로 보내지고 이후 message broker에 의해 핸들된다.
  6. *message broker은 반환된 값과 일치하는 모든 구독자들을 찾아 **“clientOutboundChannel”*을 통해 MESSAGE command를 보낸다. 여기서 “clientOutboundChannel”은 메세지가 STOMP frame으로 다시 인코딩되어 WebSocket 연결쪽에 보내는 역할을 한다.

26.4.5 Annotated Controllers

  • @MessageMapping : 목적지로 메세지를 라우팅해준다. @MessageMapping은 다양한 signatures(서명)을 갖을 수 있는데 자세한 내용은 공식문서를 참고하기로 하자. @MessageMapping 메소드에서 리턴된 값은 MessageConverter를 통해 payload로 serialized 되고, 이후 “brokerChannel”에 message가 보내지게 된다.
  • @SubscribeMapping : @MesageMapping이 destination을 특정하는 반면 @SubscribeMapping은 메세지 정보만 포함하고 있다. 이 annotation을 사용하게 되면 컨트롤러의 리턴 값이 “brokerChannel”이 아닌 “clientOutboundChannel”로 보내지게 된다. 따라서 broker을 통해서 broadcasting이 되는 것이 아닌, 클라이언트에게 직접적으로 메세지가 전달되게 된다. 이는 request-reply가 one-off로 끝날 때 사용하기 유용하다.
  • @MessageExceptionHandler : 컨트롤러에 해당 핸들러를 적용할 수 있다. 이를 더 전역적으로 관리하고 싶다면 @ControllerAdvice를 선언하여 사용하자.</aside>클라이언트는 메세지의 내용과 누가 해당 메세지를 받을 것인가에 대한 정보를 알려주기 위해 SENDSUBSCRIBE 커맨드 사용이 가능하다. 이는 pub-sub 메커니즘을 사용하여 브로커를 통해 다른 클라이언트에게 메세지를 보내거나 수행되어야 할 기능들을 서버에 요청하기 위한 메세지 송신을 가능하게 한다.
    •  

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 42일 차  (0) 2022.10.31
항해 99 38일차  (1) 2022.10.27
항해 99 37일차  (0) 2022.10.26
항해 99 36일차  (0) 2022.10.25
항해 99 35일차  (0) 2022.10.24

상속관계 매핑

  • 객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)
  • 그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
  • 상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

조인 전략

  • @Inheritance(strategy = InheritanceType.JOINED)
    • 부모 클래스에 지정. 조인 전략이므로 InheritanceType.JOINED 설정
  • @DiscriminatorColumn(name = "DTYPE")
    • 구분 컬럼 지정. Default값이 DTYPE이므로 name 속성은 생략 가능
  • @DiscriminatorValue("TEST")
    • 구분 컬럼에 입력할 값 지정. Default값으로 엔티티 이름 사용
  • @PrimaryKeyJoinColumn(name = "Album_ID")
    • Default로 자식 테이블은 부토 테이블 id 컬럼명을 그대로 사용하나, 변경시 해당 설정값 추가
  • 장점
    • 테이블의 정규화
    • 외래 키 참조 무결성 제약조건 활용 가능
    • 저장공간을 효율적으로 사용 가능
  • 단점
    • 조회시 잦은 조인으로 인해 성능 저하 가능성
    • 복잡한 조회 쿼리
    • 데이터 등록 시, 두번 실행되는 INSERT문

단일 테이블 전략

  • 하나의 테이블을 사용하며 구분 컬럼(DTYPE)을 활용해 데이터를 활용하는 전략
  • @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    • 부모 클래스에 지정. 단일 테이블 전략이므로 InheritanceType.SINGLE_TABLE 설정
  • 장점
    • 조인이 사용되지 않아 빠른 조회 성능
    • 단순한 조회 쿼리
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용
    • 높은 테이블이 커질 가능성으로 인해 오히려 조회 성능이 안좋아질 수 있음

구현 클래스마다 테이블 전략

  • 자식 엔티티마다 테이블 생성하는 전략
  • 추천하지 않는 전략
  • @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
    • 부모 클래스에 지정. 구현 클래스마다 테이블 전략이므로 InheritanceType.TABLE_PER_CLASS 설정
      • 장점
        • 서브 타입을 구분해서 처리할 때 효과적
        • not null 제약조건 사용 가능
      • 단점
        • 여러 자식 테이블 함께 조회시 성능 문제(UNION을 사용함)
        • 자식 테이블을 통합해 쿼리가 어려움

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 42일 차  (0) 2022.10.31
항해 99 39일차  (0) 2022.10.28
항해 99 37일차  (0) 2022.10.26
항해 99 36일차  (0) 2022.10.25
항해 99 35일차  (0) 2022.10.24

Redis란?

 

Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템 (DBMS)입니다.

데이터베이스, 캐시, 메세지 브로커로 사용되며 인메모리 데이터 구조를 가진 저장소입니다.

 

* db-engines.com 에서 key, value 저장소 중 가장 순위가 높습니다.

 

캐시 서버(Cache Server)

 

데이터 베이스는 데이터를 물리 디스크에 직접 쓰기 때문에 서버에 문제가 발생하여 다운되더라도 데이터가 손실되지 않습니다. 하지만 매번 디스크에 접근해야 하기 때문에 사용자가 많아질수록 부하가 많아져서 느려질 수 있는데요.

일반적으로 서비스 운영 초반이거나 규모가 작은, 사용자가 많지 않은 서비스의 경우에는 WEB - WAS - DB 의 구조로도 데이터 베이스에 무리가 가지 않습니다.

하지만 사용자가 늘어난다면 데이터 베이스가 과부하 될 수 있기 때문에 이때 캐시 서버를 도입하여 사용합니다.

그리고 이 캐시 서버로 이용할 수 있는 것이 바로 Redis 입니다.

 

캐시는 한번 읽어온 데이터를 임의의 공간에 저장하여 다음에 읽을 때는 빠르게 결괏값을 받을 수 있도록 도와주는 공간입니다.

같은 요청이 여러 번 들어오는 경우 매번 데이터 베이스를 거치는 것이 아니라 캐시 서버에서 첫 번째 요청 이후 저장된 결괏값을 바로 내려주기 때문에 DB의 부하를 줄이고 서비스의 속도도 느려지지 않는 장점이 있습니다.

 

캐시 서버는 Look aside cache 패턴과 Write Back 패턴이 존재합니다.

 

- Look aside cache

1. 클라이언트가 데이터를 요청

2. 웹서버는 데이터가 존재하는지 Cache 서버에 먼저 확인

3. Cache 서버에 데이터가 있으면 DB에 데이터를 조회하지 않고 Cache 서버에 있는 결과값을 클라이언트에게 바로 반환 (Cache Hit)

4. Cache 서버에 데이터가 없으면 DB에 데이터를 조회하여 Cache 서버에 저장하고 결과값을 클라이언트에게 반환 (Cache Miss)

 

- Write Back 

1. 웹서버는 모든 데이터를 Cache 서버에 저장

2. Cache 서버에 특정 시간 동안 데이터가 저장됨

3. Cache 서버에 있는 데이터를 DB에 저장

4. DB에 저장된 Cache 서버의 데이터를 삭제

 

* insert 쿼리를 한 번씩 500번 날리는 것보다 insert 쿼리 500개를 붙여서 한 번에 날리는 것이 더 효율적이라는 원리입니다.

* 이 방식은 들어오는 데이터들이 저장되기 전에 메모리 공간에 머무르는데 이때 서버에 장애가 발생하여 다운된다면 데이터가 손실될 수 있다는 단점이 있습니다.

 

 Redis의 특징

 

  • Key, Value 구조이기 때문에 쿼리를 사용할 필요가 없습니다.
  • 데이터를 디스크에 쓰는 구조가 아니라 메모리에서 데이터를 처리하기 때문에 속도가 빠릅니다.
  • String, Lists, Sets, Sorted Sets, Hashes 자료 구조를 지원합니다.

    String : 가장 일반적인 key - value 구조의 형태입니다.

    Sets : String의 집합입니다. 여러 개의 값을 하나의 value에 넣을 수 있습니다. 포스트의 태깅 같은 곳에 사용될 수 있습니다.

    Sorted Sets : 중복된 데이터를 담지 않는 Set 구조에 정렬 Sort를 적용한 구조로 랭킹 보드 서버 같은 구현에 사용할 수 있습니다.

    Lists : Array 형식의 데이터 구조입니다. List를 사용하면 처음과 끝에 데이터를 넣고 빼는 건 빠르지만 중간에 데이터를 삽입하거나 삭제하는 것은 어렵습니다.

 

  • Single Threaded 입니다.
    : 한 번에 하나의 명령만 처리할 수 있습니다. 그렇기 때문에 중간에 처리 시간이 긴 명령어가 들어오면 그 뒤에 명령어들은 모두 앞에 있는 명령어가 처리될 때까지 대기가 필요합니다.
    (하지만 get, set 명령어의 경우 초당 10만 개 이상 처리할 수 있을 만큼 빠릅니다.)

Redis 사용에 주의할 점

  • 서버에 장애가 발생했을 경우 그에 대한 운영 플랜이 꼭 필요합니다.
    : 인메모리 데이터 저장소의 특성상, 서버에 장애가 발생했을 경우 데이터 유실이 발생할 수 있기 때문입니다.
  • 메모리 관리가 중요합니다.
  • 싱글 스레드의 특성상, 한 번에 하나의 명령만 처리할 수 있습니다. 처리하는데 시간이 오래 걸리는 요청, 명령은 피해야 합니다.

 

 

이 외에도 Master-Slave 형식의 데이터 이중화 구조에 대한 Redis Replication, 분산 처리를 위한 Redis cluster, 장애 복구 시스템 Redis Sentinel, Redis Topology, Redis Sharding, Redis Failover 등의 Redis를 더 효율적으로 사용하기 위한 개념들이 존재합니다. 

 

 

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 39일차  (0) 2022.10.28
항해 99 38일차  (1) 2022.10.27
항해 99 36일차  (0) 2022.10.25
항해 99 35일차  (0) 2022.10.24
항해 99(9기) 5주차 WTL 회고  (0) 2022.10.23

3. 멤버 권한줘서 컨트롤러별 접근막기

 

@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors();

    http.csrf().disable()

            .exceptionHandling()
            .authenticationEntryPoint(authenticationEntryPointException)
            .accessDeniedHandler(accessDeniedHandlerException)

            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)


            .and()
            .authorizeRequests()
            .antMatchers("/api/member/**").permitAll()
            .antMatchers("/api/post/**").permitAll()
            .antMatchers("/api/comment/**").permitAll()
            .antMatchers("/api/auth/**").hasAnyRole("ROLE_ADMIN","ROLE_MEMBER")
            .antMatchers("/api/admin/**").hasRole("ROlE_ADMIN")
            .antMatchers("/v2/api-docs",
                    "/swagger-resources",
                    "/swagger-resources/**",
                    "/configuration/ui",
                    "/configuration/security",
                    "/swagger-ui.html",
                    "/webjars/**",
                    "/v3/api-docs/**",
                    "/swagger-ui/**").permitAll()
            .anyRequest().authenticated()

            .and()
            .addFilter(corsConfig.corsFilter())
            .apply(new JwtSecurityConfiguration(SECRET_KEY, tokenProvider, userDetailsService));

Security에서 제공하는 hasRole 을 사용하여 권한이 필요한 url을 등록해준다.

.antMatchers("/api/auth/**").hasAnyRole("ROLE_ADMIN","ROLE_MEMBER")
.antMatchers("/api/admin/**").hasRole("ROlE_ADMIN")

Security에서 제공하는 타입으로 변경.

public enum Authority {
    ROLE_ADMIN,
    ROLE_MEMBER,
    ROLE_GUEST
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    Authority memberRole=member.getRole();
    SimpleGrantedAuthority authority = new SimpleGrantedAuthority(memberRole.toString());
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(authority);
    return authorities;
}

 

 

 

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 38일차  (1) 2022.10.27
항해 99 37일차  (0) 2022.10.26
항해 99 35일차  (0) 2022.10.24
항해 99(9기) 5주차 WTL 회고  (0) 2022.10.23
항해 99 33일차  (0) 2022.10.21
1. member와의 연관관계 끊기
2. SOFT_DELETE 적용해보기
3. 멤버 권한줘서 컨트롤러별 접근막기

2.JPA + Hibernate 개발 환경에서의 구현

Hibernate를 사용하는 애플리케이션에서 Soft Delete를 구현하는 방법과 발생할 수 있는 문제들을 해결할 수 있는 방법들에 대해서 알아보겠습니다.

개발 환경

  • Spring Boot 2.5.10
  • Postgresql 14.1
  • Java 11(Open Jdk Azul)

구현

@Entity
@Where(clause = "deleted = false")
@SQLDelete(sql = "UPDATE posts SET deleted = true WHERE id = ?")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Posts {

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

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private boolean deleted;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "post", cascade = CascadeType.REMOVE)
    private List<Comments> comments = new ArrayList<>();

    public Posts(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void delete() {
        this.deleted = true;
    }

    public void addComment(Comments comment) {
        this.comments.add(comment);
    }

}
@Entity
@Where(clause = "deleted = false")
@SQLDelete(sql = "UPDATE comments SET deleted = true WHERE id = ?")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comments {

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

    @Column(nullable = false)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Posts post;

    @Column(nullable = false)
    private boolean deleted;

    public Comments(String content, Posts post) {
        this.content = content;
        this.post = post;
        this.post.addComment(this);
    }

    public void delete() {
        this.deleted = true;
    }
    
}

예제 소스 코드에서 사용할 게시글과 댓글 엔티티 클래스입니다. soft delete를 구현하기 위해 boolean 타입의 필드로 삭제 유무를 구분하고 메소드를 통해 객체의 상태를 변경하여 삭제합니다.

@Where(clause = "deleted = false")

엔티티 조회 쿼리의 where절에 반드시 포함되는 조건을 설정할 수 있습니다. 개발자의 실수로 쉽게 누락할 수 있기 때문에 애노테이션을 사용해서 글로벌하게 설정하는 것이 좋습니다. 또한 Lazy Loading으로 발생하는 조회 쿼리의 where절에 조건을 포함시키기 위해서는 반드시 사용해야합니다. 하지만 JPQL 또는 HQL이 아닌 native SQL을 사용할 때는 적용되지 않기 때문에 주의해야합니다.

@SQLDelete(sql = "UPDATE comments SET deleted = true WHERE id = ?")

'항해 99(9기) > 항해 일일' 카테고리의 다른 글

항해 99 37일차  (0) 2022.10.26
항해 99 36일차  (0) 2022.10.25
항해 99(9기) 5주차 WTL 회고  (0) 2022.10.23
항해 99 33일차  (0) 2022.10.21
항해 99 32일차  (0) 2022.10.20

+ Recent posts