
실제 API를 작성해보겠다.
1. 전체적인 구조
Unity
│
REST API
│
▼
RoomController
│
▼
RoomService
│ │
│ └──────────────┐
▼ ▼
RoomRepository UserRepository
(Redis) (JPA)
│ │
▼ ▼
Redis MySQL
│ │
현재 로비 상태 User
| 데이터 | 저장 위치 | 이유 |
| User | MySQL | 영구적으로 존재해야 함 |
| 현재 생성된 Room | Redis | 방이 없어지면 필요 없는 임시 상태 |
| 현재 방 참가자 | Redis | 입장/퇴장에 따라 계속 변하는 현재 상태 |
2. 기능별 흐름
- [방 만들기]
- Unity → POST /rooms
- Spring → Redis에 Room 생성
- [방 들어가기]
- Unity → POST /rooms/1/players
- Spring → MySQL에서 User 존재 확인 → Redis의 Room #1에 userId 추가
- [현재 방 참가자]
- Unity → GET /rooms/1/players
- Spring → Redis에서 Room #1 참가자 조회
- [방 나가기]
- Unity → DELETE /rooms/1/players/me
- Spring → Redis에서 해당 참가자 제거
3. 패키지 구조
com.mafia
├── controller
│ └── RoomController.java
│
├── service
│ └── RoomService.java
│
├── repository
│ ├── RoomRepository.java
│ └── UserRepository.java
│
├── domain
│ └── room
│ ├── Room.java
│ └── RoomPlayer.java
│
├── entity
│ └── User.java
│
└── dto
└── room
├── CreateRoomRequest.java
├── JoinRoomRequest.java
├── RoomResponse.java
└── RoomPlayerResponse.java
4. Redis에서 사용할 객체
Room #1
│
└── players
├── RoomPlayer(10, "철수")
├── RoomPlayer(11, "영희")
└── RoomPlayer(12, "민수")
이런식으로 사용 예정
RoomPlayer
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class RoomPlayer {
private Long userId;
private String nickname;
private boolean ready;
}
Room
package com.lobbytest.domain;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Getter
public class Room {
private Long id;
private String title;
private Long hostUserId;
private int maxPlayers;
private List<RoomPlayer> players = new ArrayList<>();
public Room(Long id, String title, Long hostUserId, int maxPlayers) {
this.id = id;
this.title = title;
this.hostUserId = hostUserId;
this.maxPlayers = maxPlayers;
this.players = new ArrayList<>();
}
public void addPlayer(RoomPlayer player) {
players.add(player);
}
public void removePlayer(Long userId) {
players.removeIf(player -> player.getUserId().equals(userId));
}
public boolean isFull() {
return players.size() >= maxPlayers;
}
public boolean containsPlayer(Long userId) {
return players.stream().anyMatch(player -> player.getUserId().equals(userId));
}
}
Room 하나를 Redis에 하나의 값으로 저장
Key
room:1
Value
{
"id": 1,
"title": "초보방",
"hostUserId": 10,
"maxPlayers": 4,
"players": [
{
"userId": 10,
"nickname": "철수",
"ready": false
},
{
"userId": 11,
"nickname": "영희",
"ready": false
}
]
}
다른 방도:
room:2
room:3
room:4
이런 식으로 존재.
5. Redis Repository
package com.lobbytest.repository;
import com.lobbytest.domain.Room;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class RoomRepository {
private final RedisTemplate<String, Room> redisTemplate;
private static final String PREFIX = "room:";
public void save(Room room) {
redisTemplate.opsForValue().set(PREFIX + room.getId(), room);
}
public Room findById(Long roomId) {
return redisTemplate.opsForValue().get(PREFIX + roomId);
}
public void delete(Long roomId) {
redisTemplate.delete(PREFIX + roomId);
}
public Long generateRoomId() {
return redisTemplate.opsForValue().increment("room:id:sequence");
}
}
그러면:
roomRepository.save(room);
하면 Redis에:
room:1 → Room 객체
가 들어가는 것이다.
redisTemplate 의존성 주입 관련

Could not autowire. No beans of 'RedisTemplate<String, Room>' type found.
즉 Spring 입장에서는:
private final RedisTemplate<String, Room> redisTemplate;
를 보고
“RedisTemplate<String, Room>을 주입해 달라는 거네.
근데 내가 관리하는 Bean 중에 그런 타입이 없는데?”
라는 상태인 것.
1. build.gradle이 다음의 의존성이 있는지 확인
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
2. Redis 설정 클래스 생성 하고 작성
com.lobbytest
├── config
│ └── RedisConfig.java
├── controller
├── service
├── repository
└── domain
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Room> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Room> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
그러면 Spring 시작할 때:
RedisConfig
↓
@Bean
↓
RedisTemplate<String, Room> 생성
↓
Spring Container에 Bean 등록
되고, 네 RoomRepository로 생성자 주입이 가능해진다.
그런데 Redis는 Java의 Room 객체를 그대로 이해하는 게 아니기 때문에 직렬화(Serialization) 설정이 필요하다.
우리가 원하는 건 대략:
<Java>
Room 객체
{
id = 1
title = "초보방"
players = ...
}
↓ 직렬화
<Redis>
Key "room:1"
Value
JSON 형태
그래서 실제 RedisConfig는 이런 식으로 잡는 게 좋다.
package com.lobbytest.config;
import com.lobbytest.domain.Room;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Room> roomRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Room> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key: "room:1" 같은 문자열
template.setKeySerializer(new StringRedisSerializer());
// Value: Room 객체를 JSON으로 변환
template.setValueSerializer(new JacksonJsonRedisSerializer<>(Room.class));
return template;
}
}
generateRoomId()
MySQL의 JPA Entity였다면:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
처럼 DB가 ID를 자동 생성해준다.
그런데 지금 Room은 MySQL에 저장하는 JPA Entity가 아니라 Redis에서 관리하는 객체라서 @GeneratedValue를 사용할 수 없다.
➡️ 그래서 방 번호를 우리가 만들어야 한다.
Redis에는 숫자를 원자적으로 1씩 증가시키는 기능이 있다.
Spring에서는 RedisTemplate의 increment()로 사용할 수 있다.
다만 지금 우리가 만든 RedisTemplate<String, Room>으로 이 카운터까지 처리하려고 하면 타입이 안 맞는다.
이 Template은 Value가 Room이라고 정해놨기 때문이다.
RedisTemplate<String, Room>
↑
Room 전용
따라서 구현할 때는 방 ID 카운터용으로 StringRedisTemplate을 별도로 사용하는 게 깔끔하다고는 하다.
private final StringRedisTemplate stringRedisTemplate;
public Long generateRoomId() {
return stringRedisTemplate
.opsForValue()
.increment("room:id:sequence");
}
StringRedisTemplate은 Spring Boot Redis 설정이 되어 있으면 일반적으로 자동 구성해주기 때문에,
Room 저장은 RedisTemplate<String, Room>, ID 같은 단순 숫자/문자열 처리는 StringRedisTemplate으로 나누면 이해하기도 편하다고는 하는데
음 일단 이거는 패쓰
6. RoomService
방 생성
// 방 생성
@Transactional(readOnly = true)
public RoomResponseDto createRoom(CreateRoomRequestDto request) {
User user = userRepository
.findById(request.getUserId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다."));
// 새로운 룸 생성
Long roomId = roomRepository.generateRoomId();
Room room = new Room(roomId, request.getTitle(), user.getId(), request.getMaxPlayers());
// 방을 만든 사람은 자동으로 해당 방에 입장
RoomPlayer host = new RoomPlayer(user.getId(), user.getNickname(), false);
room.addPlayer(host);
roomRepository.save(room);
return RoomResponseDto.from(room);
}
DTO
package com.lobbytest.dto.room;
import lombok.Getter;
@Getter
public class CreateRoomRequestDto {
private Long userId;
private String title;
private int maxPlayers;
}
package com.lobbytest.dto.room;
import com.lobbytest.domain.Room;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RoomResponseDto {
private Long id;
private String title;
private Long hostUserId;
private int maxPlayers;
private int currentPlayers;
public static RoomResponseDto from(Room room) {
return new RoomResponseDto(
room.getId(),
room.getTitle(),
room.getHostUserId(),
room.getMaxPlayers(),
room.getPlayers().size()
);
}
}
방 참가
// 방 참가
public RoomResponseDto joinRoom(Long roomId, JoinRoomRequestDto request) {
Room room = roomRepository.findById(roomId);
if (room == null) throw new IllegalArgumentException("존재하지 않는 방입니다.");
if (room.isFull()) throw new IllegalStateException("방이 가득 찼습니다.");
if (room.containsPlayer(request.getUserId())) throw new IllegalStateException("이미 참가 중입니다.");
User user = userRepository
.findById(request.getUserId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다."));
RoomPlayer player = new RoomPlayer(user.getId(), user.getNickname(), false);
room.addPlayer(player);
roomRepository.save(room);
return RoomResponseDto.from(room);
}
DTO
package com.lobbytest.dto.room;
import lombok.Getter;
@Getter
public class JoinRoomRequestDto{
private Long userId;
}
방 나가기
Service에서는:
// 방 나가기
public void leaveRoom(Long roomId, Long userId) {
Room room = roomRepository.findById(roomId);
if (room == null) throw new IllegalArgumentException("존재하지 않는 방입니다.");
if (!room.containsPlayer(userId)) throw new IllegalStateException("해당 방에 참가 중이지 않습니다.");
room.removePlayer(userId);
roomRepository.save(room);
}
현재 방 참가자 조회
// 현재 룸 참가자 조회
public List<RoomPlayerResponseDto> getPlayers(Long roomId) {
Room room = roomRepository.findById(roomId);
if (room == null) throw new IllegalArgumentException("존재하지 않는 방입니다.");
return room.getPlayers()
.stream()
.map(RoomPlayerResponseDto::from)
.toList();
}
Unity에서:
GET /rooms/1/players
하면:
[
{
"userId": 10,
"nickname": "철수",
"ready": true
},
{
"userId": 11,
"nickname": "영희",
"ready": false
}
]
를 받는 식.
DTO
package com.lobbytest.dto.room;
import com.lobbytest.domain.RoomPlayer;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class RoomPlayerResponseDto {
private Long userId;
private String nickname;
private boolean ready;
public static RoomPlayerResponseDto from(RoomPlayer roomPlayer) {
return new RoomPlayerResponseDto(
roomPlayer.getUserId(),
roomPlayer.getNickname(),
roomPlayer.isReady()
);
}
}
7. Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/rooms")
public class RoomController {
private final RoomService roomService;
@PostMapping
public ResponseEntity<RoomResponseDto> createRoom(
@RequestBody CreateRoomRequestDto request
) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(roomService.createRoom(request));
}
@PostMapping("/{roomId}/players")
public ResponseEntity<RoomResponseDto> joinRoom(
@PathVariable Long roomId,
@RequestBody JoinRoomRequestDto request
) {
return ResponseEntity.ok(roomService.joinRoom(roomId, request));
}
@GetMapping("/{roomId}/players")
public ResponseEntity<List<RoomPlayerResponseDto>> getPlayers(
@PathVariable Long roomId
) {
return ResponseEntity.ok(roomService.getPlayers(roomId));
}
@DeleteMapping("/{roomId}/players/{userId}")
public ResponseEntity<Void> leaveRoom(
@PathVariable Long roomId,
@PathVariable Long userId
) {
roomService.leaveRoom(roomId, userId);
return ResponseEntity.noContent().build();
}
}
현재 API 목록
| 방 생성 | POST | /api/v1/rooms |
| 방 참가 | POST | /api/v1/rooms/{roomId}/players |
| 현재 참가자 | GET | /api/v1/rooms/{roomId}/players |
| 방 나가기 | DELETE | /api/v1/rooms/{roomId}/players/{userId} |
'유니티 공부 > Spring 연동' 카테고리의 다른 글
| 로비 테스트 04 : 테스트 전에 간단 유저 시스템 만들기 (0) | 2026.09.29 |
|---|---|
| 로비 테스트 03 : 방 목록 조회 API 추가하기 (0) | 2026.09.29 |
| 로비 테스트 01 : Redis가 뭐고 왜 쓸것인가 (0) | 2026.09.28 |
| 간단한 유저 관리 앱 만들기 02 : UI 구성 및 연동 & 테스트 (0) | 2026.09.27 |
| 간단한 유저 관리 앱 만들기 01 : 유니티에서 API 요청하기 (0) | 2026.09.27 |
