๐ 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 ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ
โ (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://:9093 → Kafka ์ปจํธ๋กค๋ฌ(๋ธ๋ก์ปค ๊ฐ ๋ฆฌ๋ ์ ์ถ ๋ฑ)๋ฅผ ์ํ ๋ฆฌ์ค๋.
- 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๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ด ๋ ํจ์จ์ .
'ํ์ต ๊ธฐ๋ก (Learning Logs) > Today I Learned' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
โจ Operating Systems: Three Easy Pieces โจ - ๊ฐ์ํ (0) | 2025.03.19 |
---|---|
๋น ๋ฐ์ดํฐ (0) | 2025.03.18 |
์ธ๊ณต ์ง๋ฅ์ ์คํํ๊ธฐ ์ํ ๊ธฐ์ (0) | 2025.03.09 |
์ฑํ (0) | 2025.03.03 |
์ธ๊ณต์ง๋ฅ์ ์ ๋ขฐ์ฑ (0) | 2025.03.02 |