๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ•™์Šต ๊ธฐ๋ก (Learning Logs)/Today I Learned

Spring Boot + STOMP๋กœ ์นด์นด์˜คํ†ก ์Šคํƒ€์ผ ์ฑ„ํŒ…๋ฐฉ ๋งŒ๋“ค๊ธฐ

๐Ÿ“Œ Spring Boot + STOMP๋กœ ์นด์นด์˜คํ†ก ์Šคํƒ€์ผ ์ฑ„ํŒ…๋ฐฉ ๋งŒ๋“ค๊ธฐ

๐Ÿš€ STOMP + WebSocket์„ ํ™œ์šฉํ•˜์—ฌ "์นด์นด์˜คํ†ก ๊ฐ™์€ ์ฑ„ํŒ…๋ฐฉ ์‹œ์Šคํ…œ"์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
๐Ÿ“Œ STOMP๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ "์ฑ„ํŒ…๋ฐฉ์„ ์ƒ์„ฑํ•˜๊ณ  ๊ตฌ๋…ํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ!"

 

 

๊ตฌ์กฐ

src/main/java/com/example/chat/
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ WebSocketConfig.java       # WebSocket + STOMP ์„ค์ •
โ”œโ”€โ”€ controller/
โ”‚   โ”œโ”€โ”€ ChatController.java        # REST API (์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ/์กฐํšŒ)
โ”‚   โ”œโ”€โ”€ StompChatController.java   # STOMP ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
โ”œโ”€โ”€ model/
โ”‚   โ”œโ”€โ”€ ChatRoom.java              # ์ฑ„ํŒ…๋ฐฉ ๊ฐ์ฒด
โ”‚   โ”œโ”€โ”€ ChatMessage.java           # ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ๊ฐ์ฒด
โ”œโ”€โ”€ service/
โ”‚   โ”œโ”€โ”€ ChatService.java           # ์ฑ„ํŒ…๋ฐฉ ๊ด€๋ฆฌ ๋กœ์ง
โ””โ”€โ”€ repository/
    โ”œโ”€โ”€ ChatRoomRepository.java    # ์ฑ„ํŒ…๋ฐฉ ์ €์žฅ์†Œ (๋ฉ”๋ชจ๋ฆฌ)

 


HTTP, WebSocket 

HTTP, WebSocket ๋‘˜๋‹ค tcp๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

WebSocket์€ HTTP์™€ ๋‹ค๋ฅด๊ฒŒ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•œ๋‹ค.

 


SpringBoot์—์„œ WebSocket์œผ๋กœ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ

์ „์ œ

c1, c2, c3, c4๋Š” ํด๋ผ์ด์–ธํŠธ๋‹ค.

์„œ๋ฒ„๋Š” 1๋Œ€(๋‹จ์ผ ์„œ๋ฒ„)์ด๋‹ค.

 

ํ๋ฆ„

1) ๊ตฌ๋… ์š”์ฒญ

์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฑ„ํŒ…๋ฐฉ ์ด๋ฆ„์— ๋”ฐ๋ผ http๋กœ ๊ตฌ๋…์š”์ฒญ์„ ํ•œ๋‹ค.

์„œ๋ฒ„๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ํด๋ผ์ด์–ธํŠธ๋ณ„๋กœ ์ฑ„ํŒ…๋ฐฉ์ด๋ฆ„์— ๋”ฐ๋ผ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•œ๋‹ค.

 

2) c1์ด room1์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ƒ„, c2, c3๋„ ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›์•„์•ผํ•จ

์„œ๋ฒ„์— ์žˆ๋Š” ๋ธŒ๋กœ์ปค๋Š” topic == ์ฑ„ํŒ…๋ฐฉ์ด๋ฆ„์— c1์ด ๋ณด๋‚ธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.

room1์„ ๊ตฌ๋…ํ•˜๋Š” c2, c3์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•œ๋‹ค.

 

 


STOMP

STOMP๋Š” ์›น์†Œ์ผ“์œผ๋กœ๋งŒ springboot์—์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด

์ผ์ผ์ด ๋‚ด๊ฐ€ Session, SessionManager๋ฅผ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค.

์ด๋Ÿฐ ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ์ด๋ฏธ ๋งŒ๋“ค์–ด ๋†“์€ ๊ฒƒ์ด STOMP ํ”„๋กœํ† ์ฝœ์ด๋‹ค.

 

stomp๋„ ์„œ๋ฒ„ ๋‚ด client session ์ •๋ณด๋ฅผ ๋‚ด๋ถ€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋‹ค๋ฅธ ์„œ๋ฒ„์™€ ๊ณต์œ ๋˜์ง€ ์•Š๋Š”๋‹ค.

๋”ฐ๋ผ์„œ ๋ฉ€ํ‹ฐ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ๋Š” stomp๊ฐ€ ์ œ๊ณตํ•˜๋Š” Broker๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

 

 


๋ฉ€ํ‹ฐํ™˜๊ฒฝ์—์„œ Broker ์—ญํ• 

๋ฉ€ํ‹ฐ ํ™˜๊ฒฝ์—์„œ ๊ทธ๋Œ€๋กœ Broker๋ฅผ ์‚ฌ์šฉ์‹œ ๋ฌธ์ œ์ : 

  • ๋ฉ€ํ‹ฐํ™˜๊ฒฝ์—์„œ๋Š” ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ์— ์˜ํ•ด์„œ ๋žœ๋ค์œผ๋กœ ์„œ๋ฒ„๊ฐ€ ๋ฐฐ์ • ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์—, client๊ฐ€ ์–ด๋–ค ์„œ๋ฒ„๋กœ ๊ฐˆ์ง€ ๋ชจ๋ฅธ๋‹ค.
  • ๋‹ค๋ฅธ ์„œ๋ฒ„์— ์žˆ๋Š” ๊ฐ™์€ room1์„ ๊ตฌ๋…ํ•˜๋Š” client์—๊ฒŒ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์—†๋‹ค.

 

์˜ˆ์‹œ

๊ฐ ํด๋ผ์ด์–ธํŠธ ๋ณ„๋กœ ๊ตฌ๋…์ค‘์ธ ์ฑ„ํŒ…๋ฐฉ ๋ชฉ๋ก

๊ฐ ํด๋ผ์ด์–ธํŠธ๋Š” ์ฑ„ํŒ…๋ฐฉ์„ ๊ตฌ๋…์ค‘์ด๋‹ค.

ํด๋ผ์ด์–ธํŠธ๋Š” ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ์— ์˜ํ•ด ์–ด๋Š ์„œ๋ฒ„์— ๋ฐฐ์ •๋ ์ง€ ๋ชจ๋ฅธ๋‹ค.

 

 

 


๋ฉ€ํ‹ฐ ์„œ๋ฒ„ ํ™˜๊ฒฝ(์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ) ๊ฐ™์€ ์ฑ„ํŒ…๋ฐฉ์„ ๊ตฌ๋…ํ•˜๋Š” client์—๊ฒŒ ๋ฉ”์„ธ์ง€๋ฅผ ์ „ํŒŒํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: ์™ธ๋ถ€์˜ ๋…๋‹จ์ ์ธ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ๊ฐ ์„œ๋ฒ„๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ์ „ํŒŒํ•˜๋ฉด ๋œ๋‹ค.

 

 

 

Redis์˜ Pub/Sub(Publish/Subscribe) ๊ธฐ๋Šฅ์€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค์™€ ๊ฐ™์€ ์—ญํ• 

ํŠน์ • ์ฑ„๋„(Channel) == ์ฑ„ํŒ…๋ฐฉ์ด๋ฆ„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด,

ํ•ด๋‹น ์ฑ„๋„์„ ๊ตฌ๋…(Subscribe)ํ•œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•จ

 


Redis Pub/Sub ๊ฐœ๋… ๋ฐ ์›๋ฆฌ

Redis์˜ Pub/Sub ๋ชจ๋ธ์€ ๋ฉ”์‹œ์ง€ ํ(Message Queue)์™€ ๋‹ฌ๋ฆฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹.

1.1 ๋™์ž‘ ์›๋ฆฌ

  • Publisher (๋ฐœํ–‰์ž): ํŠน์ • ์ฑ„๋„(Channel) ์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰(PUBLISH)ํ•˜๋ฉด, ํ•ด๋‹น ์ฑ„๋„์„ ๊ตฌ๋…ํ•œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ .
  • Subscriber (๊ตฌ๋…์ž): ํŠน์ • ์ฑ„๋„์„ ๊ตฌ๋…(SUBSCRIBE) ํ•˜๋ฉด, ํ•ด๋‹น ์ฑ„๋„์— ๋ฐœํ–‰๋˜๋Š” ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜์‹ .
  • Channel (์ฑ„๋„): ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœํ–‰๋˜๊ณ  ์ „๋‹ฌ๋˜๋Š” ๊ฐ€์ƒ์˜ ํ†ต๋กœ. == ์ฑ„ํŒ…๋ฐฉ ์ด๋ฆ„

room1์„ ๊ตฌ๋…ํ•œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ "Hello, Redis Pub/Sub!" ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์Œ.


terminal์—์„œ redis๋กœ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ

# Redis ์„ค์น˜
sudo apt update
sudo apt install redis -y

# Redis ์„œ๋ฒ„ ์‹คํ–‰
redis-server

# Subscriber (๊ตฌ๋…์ž) ์‹คํ–‰
redis-cli
> SUBSCRIBE my_channel


# Publisher (๋ฐœํ–‰์ž) ์‹คํ–‰
redis-cli
> PUBLISH my_channel "Hello, Redis Pub/Sub!"

 

 

Spring Boot ๊ธฐ๋ฐ˜ Redis Pub/Sub ์ฝ”๋“œ ๊ตฌํ˜„

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

 

@Configuration
@EnableCaching
public class RedisConfig {

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

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

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new PatternTopic("my_channel"));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "onMessage");
    }
}

 

@Component
public class RedisPublisher {
    private final RedisTemplate<String, Object> redisTemplate;

    public RedisPublisher(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void publish(String channel, String message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

 

 

 


 

redis:latest์—์„œ Redisson๊ณผ Redis Pub/Sub ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€

 

build.gradle

 

 

 

 

โœ… (1) Redisson ์„ค์ • (Spring Boot)

 

Redisson์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Redisson ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, redisson.yaml, Bean ๋“ฑ๋ก์ด ํ•„์š”ํ•จ

 

 

โœ… (2) Redis Pub/Sub ์„ค์ •

Pub/Sub์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด RedisTemplate๊ณผ MessageListener๋ฅผ ์„ค์ •

 

 

 

 

 


docker compose up -d

 

 

 

 

  • Kafka๋ฅผ KRaft ๋ชจ๋“œ๋กœ ์‹คํ–‰ํ•˜๋ฉด Zookeeper ์—†์ด ์ž‘๋™ํ•˜์ง€๋งŒ, ๋ฐ˜๋“œ์‹œ CLUSTER_ID๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•จ.
  • CLUSTER_ID๊ฐ€ ์—†์œผ๋ฉด Kafka๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ  Command [/usr/local/bin/dub ensure CLUSTER_ID] FAILED ! ์˜ค๋ฅ˜ ๋ฐœ์ƒ.

 

 

docker run --rm confluentinc/cp-kafka:latest kafka-storage random-uuid

-> ๋žœ๋ค ์•„์ด๋”” ์ƒ์„ฑํ•ด์คŒ

 

2025-03-19 16:49:17 ===> User
2025-03-19 16:49:17 uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
2025-03-19 16:49:17 ===> Configuring ...
2025-03-19 16:49:17 Running in KRaft mode...
2025-03-19 16:49:18 ===> Running preflight checks ... 
2025-03-19 16:49:18 ===> Check if /var/lib/kafka/data is writable ...
2025-03-19 16:49:18 ===> Running in KRaft mode, skipping Zookeeper health check...
2025-03-19 16:49:18 ===> Using provided cluster id sPjlIZ5GRP658xZlxkiDzg ...
2025-03-19 16:49:19 Exception in thread "main" java.lang.IllegalArgumentException: requirement failed: controller.listener.names must contain at least one value appearing in the 'listeners' configuration when running the KRaft controller role at scala.Predef$.require(Predef.scala:337) at kafka.server.KafkaConfig.validateControllerListenerExistsForKRaftController$1(KafkaConfig.scala:1290) at kafka.server.KafkaConfig.validateValues(KafkaConfig.scala:1352) at kafka.server.KafkaConfig.<init>(KafkaConfig.scala:1223) at kafka.server.KafkaConfig.<init>(KafkaConfig.scala:545) at kafka.tools.StorageTool$.$anonfun$execute$1(StorageTool.scala:72) at scala.Option.flatMap(Option.scala:283) at kafka.tools.StorageTool$.execute(StorageTool.scala:72) at kafka.tools.StorageTool$.main(StorageTool.scala:53) at kafka.tools.StorageTool.main(StorageTool.scala)

 

services:
    redis:
        image: redis:latest
        container_name: redis
        ports:
            - "6379:6379"
        networks:
            - webtoon

    postgres:
        image: postgres:latest
        container_name: postgres
        ports:
            - "5432:5432"
        environment:
            POSTGRES_DB: ๋””๋น„
            POSTGRES_USER: ์œ ์ €
            POSTGRES_PASSWORD: ๋น„๋ฒˆ
        volumes:
            - postgres_data:/var/lib/postgresql/data  # ๋ฐ์ดํ„ฐ ์œ ์ง€
        networks:
            - webtoon

    kafka:
        image: confluentinc/cp-kafka:latest
        container_name: kafka
        ports:
            - "9092:9092"
        environment:
            KAFKA_NODE_ID: 1
            KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_INTERNAL://:9094  # CONTROLLER ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
            KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:9092 #๋ธŒ๋กœ์ปค์šฉ ๋ฆฌ์Šค๋„ˆ
            KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
            KAFKA_PROCESS_ROLES: controller,broker #Kafka๋ฅผ ๋ธŒ๋กœ์ปค์™€ ์ปจํŠธ๋กค๋Ÿฌ ์—ญํ• ์„ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค์ •
            KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 #์ปจํŠธ๋กค๋Ÿฌ ์„ ์ถœ์„ ์œ„ํ•œ ์„ค์ •
            KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER #๋ฆฌ์Šค๋„ˆ
            KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT #๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ ๋งคํ•‘
            KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT_INTERNAL  # ๋‚ด๋ถ€ ๋ธŒ๋กœ์ปค ํ†ต์‹ ์šฉ ์„ค์ • ๋ณ€๊ฒฝ # PLAINTEXT ๋ธŒ๋กœ์ปค ๊ฐ„ ํ†ต์‹  ๋ฐฉ์‹ ์ง€์ •
            CLUSTER_ID: sPjlIZ5GRP658xZlxkiDzg  # ์ƒ์„ฑํ•œ CLUSTER_ID ์ถ”๊ฐ€
        volumes:
            - kafka_data:/var/lib/kafka/data #Kafka ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ณผ๋ฅจ ์ถ”๊ฐ€
        networks:
            - webtoon
volumes:
    postgres_data:
    kafka_data:

networks:
    webtoon:
        external: true  # ์ด๋ฏธ ์ƒ์„ฑ๋œ ๋„คํŠธ์›Œํฌ๋ฅผ ์‚ฌ์šฉ
        name: webtoon
        driver: bridge

 

๐Ÿ” PLAINTEXT๋ž€?

Kafka์—์„œ PLAINTEXT๋Š” ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์€ ์ผ๋ฐ˜ ํ…์ŠคํŠธ ํ†ต์‹  ํ”„๋กœํ† ์ฝœ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

Kafka๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ณด์•ˆ ํ”„๋กœํ† ์ฝœ์„ ์ง€์›ํ•˜๋Š”๋ฐ, PLAINTEXT๋Š” TLS(SSL) ์—†์ด ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

 

๐Ÿ“Œ Kafka์˜ listener ํ”„๋กœํ† ์ฝœ ์˜ต์…˜

ํ”„๋กœํ† ์ฝœ ์„ค๋ช… ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™” ์ธ์ฆ ๊ธฐ๋Šฅ ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ํ™˜๊ฒฝ
PLAINTEXT ์•”ํ˜ธํ™”๋˜์ง€ ์•Š์€ ์ผ๋ฐ˜ TCP ํ†ต์‹  (๊ธฐ๋ณธ๊ฐ’) โŒ ์—†์Œ โŒ ์—†์Œ ๋‚ด๋ถ€ ๋„คํŠธ์›Œํฌ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ)
SSL TLS(SSL) ์•”ํ˜ธํ™”๊ฐ€ ์ ์šฉ๋œ ํ†ต์‹  โœ… TLS ์•”ํ˜ธํ™” โŒ ์—†์Œ ๋ณด์•ˆ์ด ํ•„์š”ํ•œ ํ™˜๊ฒฝ
SASL_PLAINTEXT PLAINTEXT + ์‚ฌ์šฉ์ž ์ธ์ฆ (SASL) ์ถ”๊ฐ€ โŒ ์—†์Œ โœ… ID/PW ์ธ์ฆ ๊ฐ€๋Šฅ ๋ณด์•ˆ์ด ํ•„์š”ํ•œ ํ™˜๊ฒฝ
SASL_SSL SSL + ์‚ฌ์šฉ์ž ์ธ์ฆ (SASL) ์ถ”๊ฐ€ โœ… TLS ์•”ํ˜ธํ™” โœ… ID/PW ์ธ์ฆ ๊ฐ€๋Šฅ ์ตœ์ƒ์˜ ๋ณด์•ˆ ํ•„์š”

 

KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_INTERNAL://:9094

KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT

 

  • PLAINTEXT://:9092 ํด๋ผ์ด์–ธํŠธ(์˜ˆ: Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜)๊ฐ€ Kafka ๋ธŒ๋กœ์ปค์— ์ ‘์†ํ•˜๋Š” ๊ธฐ๋ณธ ๋ฆฌ์Šค๋„ˆ.
  • CONTROLLER://:9093Kafka ์ปจํŠธ๋กค๋Ÿฌ(๋ธŒ๋กœ์ปค ๊ฐ„ ๋ฆฌ๋” ์„ ์ถœ ๋“ฑ)๋ฅผ ์œ„ํ•œ ๋ฆฌ์Šค๋„ˆ.
  • PLAINTEXT_INTERNAL://:9094๋‚ด๋ถ€ ๋ธŒ๋กœ์ปค ๊ฐ„ ํ†ต์‹ ์„ ์œ„ํ•œ ๋ฆฌ์Šค๋„ˆ.

 

 

๐Ÿ”’ PLAINTEXT ๋Œ€์‹  SSL์„ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ

โœ… PLAINTEXT๋Š” ๋ณด์•ˆ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์—, ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” SSL์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.
Kafka ๋ธŒ๋กœ์ปค๊ฐ€ ์™ธ๋ถ€ ์ธํ„ฐ๋„ท์— ์—ฐ๊ฒฐ๋˜๋Š” ๊ฒฝ์šฐ, SSL ๋˜๋Š” SASL_SSL์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

KAFKA_LISTENERS: SSL://:9092,CONTROLLER://:9093

KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:SSL,SSL:SSL

 

 

 

 

 

 


ํ…Œ์ด๋ธ” ์„ค๊ณ„

 


๐Ÿš€ ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘: ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘

ํ…Œ์ด๋ธ”, ์™ธ๋ž˜ํ‚ค ๋งคํ•‘ vs ๊ฐ์ฒด์ง€ํ–ฅ, ์ฐธ์กฐ

  • ์ผ๋ฐ˜์ ์ธ ์ฑ„ํŒ… ์„œ๋น„์Šค๋ผ๋ฉด @ManyToOne์ด ๋” ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ ์ ํ•ฉ
  • ๋Œ€๊ทœ๋ชจ ์ฑ„ํŒ… ์„œ๋น„์Šค๋ผ๋ฉด UUID ์ €์žฅ ๋ฐฉ์‹์ด JOIN ๋ถ€๋‹ด์„ ์ค„์—ฌ์„œ ์„ฑ๋Šฅ์ด ๋” ์ข‹์Œ
  • ์ค‘๊ฐ„ ํƒ€ํ˜‘์•ˆ: UUID๋ฅผ ์ €์žฅํ•˜๋˜, ์บ์‹ฑ(Redis)๊ณผ ์กฐํ•ฉํ•ด์„œ ์„ฑ๋Šฅ ๋ณด์™„ ๊ฐ€๋Šฅ ๐Ÿš€

 

  @ManyToOne(๊ฐ์ฒด ๊ด€๊ณ„ ๋งคํ•‘) UUID ์ง์ ‘ ์ €์žฅ, ํ…Œ์ด๋ธ” ์™ธ๋ž˜ํ‚ค ๋งคํ•‘
์ฑ„ํŒ…๋ฐฉ ์ฐธ์กฐ ๋ฐฉ์‹ @ManyToOne private ChatRoomEntity chatRoomId; @Column(nullable = false) private UUID chatRoomId;
์‚ฌ์šฉ์ž ์ฐธ์กฐ ๋ฐฉ์‹ @ManyToOne private UserEntity userId; @Column(nullable = false) private UUID userId;
JOIN ์—ฌ๋ถ€ JOIN ํ•„์š” (๊ฐ์ฒด ๊ด€๊ณ„ ๋งคํ•‘) JOIN ๋ถˆํ•„์š” (UUID ์ง์ ‘ ์ €์žฅ)
JPA ๊ด€๊ณ„ ํ™œ์šฉ์„ฑ โœ… ๊ฐ์ฒด ํƒ์ƒ‰ ๊ฐ€๋Šฅ (JPA ์—ฐ๊ด€๊ด€๊ณ„) โŒ ๊ฐ์ฒด ํƒ์ƒ‰ ๋ถˆ๊ฐ€๋Šฅ (ID๋งŒ ์ €์žฅ)
์œ ์—ฐ์„ฑ ๐Ÿš€ JPA์™€ ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ์œ ๋ฆฌ (JOIN ์ตœ์†Œํ™” ๊ฐ€๋Šฅ)

 


โ“์™œ ํ…Œ์ด๋ธ” ๋งคํ•‘์—์„œ ์กฐ์ธ์ด ํ•„์š” ์—†๋‹ค๊ณ  ํ•˜๋Š”๊ฐ€?

FK ๊ฐ’์„ ๋‹จ์ˆœํžˆ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— JPA๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์ž๋™ ์กฐ์ธ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ChatParticipantEntity๋ฅผ ์กฐํšŒํ•œ ํ›„ ์ฑ„ํŒ…๋ฐฉ์ด๋‚˜ ์œ ์ € ์ •๋ณด๋ฅผ ์ถ”๊ฐ€๋กœ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด ์ˆ˜๋™์œผ๋กœ JOIN์ด ํ•„์š”ํ•จ.

 

@Column(nullable = false)
private UUID chatRoomId;  // โœ… ์ฑ„ํŒ…๋ฐฉ ID (FK)

@Column(nullable = false)
private UUID userId;  // โœ… ์ฐธ์—ฌ์ž ID (FK)
  • chatRoomId์™€ userId๊ฐ€ UUID ๊ฐ’์œผ๋กœ ์ €์žฅ๋˜์–ด ์žˆ์Œ.
  • JPA์—์„œ ๊ฐ์ฒด ์ฐธ์กฐ๋ฅผ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ ์ž๋™ ์กฐ์ธ ๋ฐœ์ƒ ์•ˆ ํ•จ.
  • findAll()์„ ํ˜ธ์ถœํ•˜๋ฉด p_chat_participants ํ…Œ์ด๋ธ”์—์„œ chatRoomId, userId ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ๋ฟ, ์—ฐ๊ด€๋œ ํ…Œ์ด๋ธ”์„ ์กฐํšŒํ•˜์ง€ ์•Š์Œ.
  • ์‹ค์ œ ์ฑ„ํŒ…๋ฐฉ ๋˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด ์ถ”๊ฐ€์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•จ.

ํ˜ธ์ถœ ๊ฒฐ๊ณผ

SELECT * FROM p_chat_participants;

 

chatroom ์ •๋ณด, user ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด jpa๋กœ ๊ฐ๊ฐ ์กฐํšŒํ•ด์•ผํ•จ

List<ChatParticipantEntity> participants = chatParticipantRepository.findAll();
for (ChatParticipantEntity participant : participants) {
    UUID chatRoomId = participant.getChatRoomId();
    UUID userId = participant.getUserId();

    ChatRoomEntity chatRoom = chatRoomRepository.findById(chatRoomId).orElse(null);
    UserEntity user = userRepository.findById(userId).orElse(null);
}
  • chatRoomRepository.findById(chatRoomId)์™€ userRepository.findById(userId)๋ฅผ ๋ณ„๋„๋กœ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•จ.
  • ์ฆ‰, JPA๊ฐ€ ์ž๋™ ์กฐ์ธ์„ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ฟผ๋ฆฌ๊ฐ€ ๋ถ„๋ฆฌ๋จ.

 

๊ฐ์ฒด ๊ด€๊ณ„ ๋งคํ•‘ ์Šคํƒ€์ผ

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "chat_room_id", nullable = false)
private ChatRoomEntity chatRoom;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private UserEntity user;

๊ฐ์ฒด ์ฐธ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด JPA๊ฐ€ ์ž๋™์œผ๋กœ ์กฐ์ธ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

ํ˜ธ์ถœ ๊ฒฐ๊ณผ

SELECT cp.*, cr.*, u.*
FROM p_chat_participants cp
JOIN p_chat_rooms cr ON cp.chat_room_id = cr.chat_room_id
JOIN p_users u ON cp.user_id = u.user_id;
  • JPA๊ฐ€ ChatRoomEntity์™€ UserEntity๋ฅผ ๊ฐ์ฒด๋กœ ์ฐธ์กฐํ•จ.
  • findAll()์„ ํ˜ธ์ถœํ•˜๋ฉด, ๊ธฐ๋ณธ์ ์œผ๋กœ JOIN ์—†์ด chat_participants ํ…Œ์ด๋ธ”๋งŒ ์กฐํšŒํ•จ.
  • ์ดํ›„ getChatRoom()์ด๋‚˜ getUser()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด **์ง€์—ฐ ๋กœ๋”ฉ(Lazy Loading)**์œผ๋กœ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋จ.
  • fetch = FetchType.EAGER๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™ ์กฐ์ธ ๋ฐœ์ƒ.

 

ChatParticipantEntity -> ๊ฐ์ฒด์ง€ํ–ฅ ๋งคํ•‘: chatRoomId, userId

package com.webtoonmaker.api.chat.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.util.UUID;

@Entity
@Table(name = "p_chat_participants")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Getter
public class ChatParticipantEntity extends BaseEntity {

    @Id
    @Column(name = "chat_participant_id", columnDefinition = "UUID", nullable = false, unique = true)
    private UUID chatParticipantId;

    @ManyToOne
    @JoinColumn(name = "chatroom_id", nullable = false)
    private ChatRoomEntity chatRoomId;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private UserEntity userId;
}

โœ… ์žฅ์ 

  • chatRoom.getParticipants() ๊ฐ์ฒด ํƒ์ƒ‰ ๊ฐ€๋Šฅ
  • user.getChatRooms() JPA ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ํ†ตํ•ด ์‰ฝ๊ฒŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • JPA ๊ด€๊ณ„ ์„ค์ •์œผ๋กœ ์ฝ”๋“œ๊ฐ€ ๋” ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ ์œ ์ง€๋จ

โŒ ๋‹จ์ 

  • JOIN ์ฟผ๋ฆฌ ํ•„์ˆ˜ → ๋Œ€๊ทœ๋ชจ ์„œ๋น„์Šค์—์„œ๋Š” ์„ฑ๋Šฅ ๋ถ€๋‹ด์ด ๋  ์ˆ˜ ์žˆ์Œ
  • ์บ์‹ฑ์ด ์—†์œผ๋ฉด ๋Š๋ ค์งˆ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

 

 

 

 

ChatParticipantEntity -> ํ…Œ์ด๋ธ”, ์™ธ๋ž˜ํ‚ค ๋งคํ•‘: chatRoomId, userId

@Entity
@Table(name = "p_chat_participants")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Getter
public class ChatParticipantEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private UUID chatRoomId;  // โœ… ์ฑ„ํŒ…๋ฐฉ ID (FK)

    @Column(nullable = false)
    private UUID userId;  // โœ… ์ฐธ์—ฌ์ž ID (FK)

    @Builder
    private ChatParticipantEntity(UUID chatRoomId, UUID userId) {
        this.chatRoomId = chatRoomId;
        this.userId = userId;
    }

    public static ChatParticipantEntity join(UUID chatRoomId, UUID userId) {
        return ChatParticipantEntity.builder()
            .chatRoomId(chatRoomId)
            .userId(userId)
            .build();
    }
}

 

โœ… ์žฅ์ 

  • JOIN์ด ํ•„์š” ์—†์Œ  ์„ฑ๋Šฅ์ด ๋›ฐ์–ด๋‚จ
  • ์ฟผ๋ฆฌ ๋‹จ์ˆœํ™” → SELECT * FROM p_chat_participants WHERE chatRoomId = ?
  • JPA๊ฐ€ ์•„๋‹Œ ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ (QueryDSL, MyBatis) ์‚ฌ์šฉ์ด ๋” ํŽธ๋ฆฌํ•จ

โŒ ๋‹จ์ 

  • chatRoom.getParticipants() ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ฐ์ฒด ํƒ์ƒ‰ ๋ถˆ๊ฐ€๋Šฅ
  • user.getChatRooms() ๊ฐ™์€ ์—ฐ๊ด€๊ด€๊ณ„ ํ™œ์šฉ ๋ถˆ๊ฐ€๋Šฅ → ๋ณ„๋„ ์ฟผ๋ฆฌ ํ•„์š”
  • ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์ฒดํฌ๊ฐ€ ํ•„์š”ํ•จ → UUID๊ฐ€ DB์— ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•จ

 


๐Ÿ”ฅ ๊ฒฐ๋ก : ์–ด๋–ค ๋ฐฉ์‹์ด ์ข‹์„๊นŒ?

โœ… JPA ์—ฐ๊ด€๊ด€๊ณ„ (@ManyToOne) ์ถ”์ฒœํ•˜๋Š” ๊ฒฝ์šฐ

  • ๊ฐ์ฒด ํƒ์ƒ‰์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ (JPA๋ฅผ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉ)
  • ์„œ๋น„์Šค ๋กœ์ง์ด ๋ณต์žกํ•˜๊ณ  ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ ์กฐ์ž‘์ด ๋งŽ์„ ๋•Œ
  • ์ฑ„ํŒ…๋ฐฉ๊ณผ ์œ ์ € ๊ฐ„ ๊ด€๊ณ„๋ฅผ ๊ฐ์ฒด ์ง€ํ–ฅ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ

โœ… UUID ์ง์ ‘ ์ €์žฅ ๋ฐฉ์‹ ์ถ”์ฒœํ•˜๋Š” ๊ฒฝ์šฐ

  • ๋Œ€๊ทœ๋ชจ ์ฑ„ํŒ… ์„œ๋น„์Šค (๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜๋Š” ๊ฒฝ์šฐ)
  • ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•˜๊ณ  JOIN์„ ์ตœ์†Œํ™”ํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • NoSQL ๊ฐ™์€ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•˜๋Š” ๊ฒฝ์šฐ

 

 

์ฑ„ํŒ…๋ฉ”์„ธ์ง€์—์„œ ์ฑ„ํŒ…๋ฐฉid๋ฅผ ํ…Œ์ด๋ธ”์กฐ์ธ? ๊ฐ์ฒด์ง€ํ–ฅ?

chatRoomId๋ฅผ ๊ฐ์ฒด ์ฐธ์กฐ๋กœ ํ• ์ง€ (ChatRoomEntity) ๋ฌธ์ž์—ด๋กœ ์œ ์ง€ํ• ์ง€ ๊ฒฐ์ •

  • ๊ฐ์ฒด ์ฐธ์กฐ (@ManyToOne):
    • ChatRoomEntity ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•  ๊ฒฝ์šฐ, ๊ฐ์ฒด์ง€ํ–ฅ์ ์œผ๋กœ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Œ.
    • ํ•˜์ง€๋งŒ ์ฑ„ํŒ…๋ฐฉ ID๋งŒ ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ๋ถˆํ•„์š”ํ•œ JOIN์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด ์„ฑ๋Šฅ์ด ๋–จ์–ด์งˆ ์ˆ˜๋„ ์žˆ์Œ.
  • ํ˜„์žฌ์ฒ˜๋Ÿผ String ์œ ์ง€:
    • ๋‹จ์ˆœํžˆ ID ๊ฐ’๋งŒ ์ €์žฅํ•˜๊ณ , chatRoomId๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ„๋„๋กœ ์กฐํšŒํ•˜๋ฉด, JOIN์ด ํ•„์š” ์—†์–ด ์„ฑ๋Šฅ์ƒ ์œ ๋ฆฌ.
    • ๋ฉ”์‹œ์ง€ ์ˆ˜๊ฐ€ ๋งŽ์„ ๊ฒฝ์šฐ, ChatRoomEntity๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋‹จ์ˆœ ID๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ .