2021. 7. 4. 20:26ㆍSpring Data/Redis With CrudRepository
Redis 에 사용자 정보를 저장, 조회, 수정, 삭제하는 예제입니다.
프로젝트 구조는 아래와 같습니다.
User.java
import java.io.Serializable;
@RedisHash("user")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 664865927712847110L;
@Id
private Long id;
@Indexed
private String userId;
private String password;
private String email;
public User(String userId, String password, String email) {
this.userId = userId;
this.password = password;
this.email = email;
}
}
소스에서 @Indexed 어노테이션을 사용하는 이유는 사용자 ID로 데이터를 조회를 위해서 어노테이션을 생성 해 줍니다.
UserService.java
package com.roopy.service.impl;
import com.roopy.exception.UserExistException;
import com.roopy.exception.UserNotFoundException;
import com.roopy.model.User;
import com.roopy.repository.UserRepository;
import com.roopy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User save(User user) throws UserExistException {
Optional<User> registeredUser = userRepository.findUserByUserId(user.getUserId());
if (registeredUser.isPresent()) {
throw new UserExistException("[" + user.getUserId() + "] 등록된 사용자 입니다.");
}
return userRepository.save(user);
}
@Override
public User update(User user) throws UserNotFoundException {
Optional<User> registeredUser = Optional.ofNullable(userRepository.findUserByUserId(user.getUserId())
.orElseThrow(() -> new UserNotFoundException("사용자 정보가 존재하지 않습니다.")));
if (registeredUser.isPresent()) {
user = userRepository.save(user);
}
return user;
}
@Override
public User findUserByUserId(String userId) throws UserNotFoundException {
Optional<User> user = Optional.ofNullable(userRepository.findUserByUserId(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보가 존재하지 않습니다.")));
return user.get();
}
@Override
public User findById(Long id) throws UserNotFoundException {
Optional<User> user = Optional.ofNullable(userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("사용자 정보가 존재하지 않습니다.")));
return user.get();
}
@Override
public void remove(Long id) {
Optional<User> user = Optional.ofNullable(userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("사용자 정보가 존재하지 않습니다.")));
userRepository.delete(user.get());
}
}
UserRepository.java
package com.roopy.repository;
import com.roopy.model.User;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findUserByUserId(String userId);
}
UserRepository 인터페이스는 CrudRepository를 상속받고 추가적으로 User 모델에서 userId에 @Indexed 설정을 하였는데 그 이유는 사용자ID로 사용자 정보를 조회합니다.
테스트
테스트는 두 가지 방식으로 진행합니다.
첫 번째는 Service Layer에 대한 JUnit 테스트를 진행하고 두 번째로는 Postman을 이용하여서 Controller에 대한 테스트를 진행합니다.
1. JUnit 테스트
UserServiceTest
package com.roopy.service;
import com.roopy.exception.UserExistException;
import com.roopy.exception.UserNotFoundException;
import com.roopy.model.User;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("UserService 테스트")
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
@DisplayName("사용자 등록")
@Order(1)
public void shouldRegisterUser() {
User user = new User("roopy", "1234", "roopy@gmail.com");
userService.save(user);
User inquiryUser = userService.findUserByUserId(user.getUserId());
Assertions.assertNotNull(inquiryUser, "사용자정보가 정상적으로 등록 되었습니다.");
}
@Test
@DisplayName("사용자 조회")
@Order(2)
public void shouldFindUser() {
// 사용자ID에 해당하는 사용자 정보 조회
User user = userService.findUserByUserId("roopy");
// ID에 해당하는 사용자 정보 조회
user =userService.findById(user.getId());
Assertions.assertEquals("roopy", user.getUserId());
}
@Test
@DisplayName("사용자 중복 등록 에러")
@Order(3)
public void isShouldThrowUserExistException() {
Assertions.assertThrows(UserExistException.class,
() -> userService.save(new User("roopy", "1234", "roopy@gmail.com")));
}
@Test
@DisplayName("사용자 정보 수정 에러")
@Order(4)
public void isShouldUserNotFoundExceptionWhenModifyUser() {
Assertions.assertThrows(UserNotFoundException.class,
() -> userService.update(new User("roopy1", "1234", "roopy@gmail.com")));
}
@Test
@DisplayName("사용자 수정")
@Order(5)
public void shouldModifyUser() {
User user = userService.findUserByUserId("roopy");
user.setEmail("martica@naver.com");
user = userService.update(user);
User inquiryUser = userService.findUserByUserId(user.getUserId());
Assertions.assertEquals("martica@naver.com", inquiryUser.getEmail());
}
@Test
@DisplayName("사용자 삭제")
@Order(6)
public void shouldRemove() {
// 사용자ID에 해당하는 사용자 정보 조회
User user = userService.findUserByUserId("roopy");
userService.remove(user.getId());
Assertions.assertThrows(UserNotFoundException.class,
() -> userService.findUserByUserId("roopy"));
}
}
테스트 순서는 다음과 같습니다.
1. 사용자 등록
2. 사용자 조회
3. 사용자 중복 등록 에러
4. 사용자 정보 수정 에러
5. 사용자 수정
6. 사용자 삭제
위의 테스트를 모두 통과한 경우 Controller 코드를 작성한다.
Service Layer의 테스트가 중요한 이유는 서비스에 대한 중요 로직을 포함하고 있기 때문에 서비스에 대한 테스트는 실무에서도 상당히 중요하다.
테스트 결과는 아래 그림과 같습니다.
2. Postman 테스트
01. 사용자 등록
사용자 등록 결과 정상적으로 데이터가 등록된 것을 Body 부분에서 확인할 수 있습니다.
정상적으로 Redis에 등록된 것을 확인 할 수 있습니다.
그리고 "user:277...:idx"라는 부분이 보일 것입니다.
이 데이터는 User.java 에서 userId key에 @Indexed 어노테이션을 선언하였기 때문에 생성된 것입니다.
02 사용자 수정
email 정보를 변경하였을 때 정상적으로 변경된 것을 Body 부분에서 확인할 수 있습니다.
Redis 수정 정보 조회 결과를 확인해 보면 정상적으로 데이터가 변경된 것을 확인할 수 있습니다.
03 사용자 조회
userId로 사용자를 조회하였을 때 정상적으로 조회되는 것을 확인할 수 있습니다.
04 사용자 삭제
사용자 삭제 시 사용자 조회 결과에서 id 값을 위의 URL에서 user 다음에 설정해주고 삭제 테스트를 한다.
Redis 조회 결과 데이터가 정상적으로 삭제된 것을 확인할 수 있다.
마지막으로 실제로 서비스에 적용했던 내용입니다.
예전에 화물차의 경로를 네이버 지도에 표시하기 위해서 Redis를 사용하였는데 컨테이너 운반 차량이 출발부터 도착할 때까지 5초 간격으로 현재 위치(위도, 경도) 정보를 서버로 보내면 서버에서는 위치 정보를 받아서 Redis 서버에 위치 정보를 저장합니다.
그리고 네이버 지도에는 WebSocket을 이용하여서 계속해서 지도상에 차량의 위치를 실시간으로 표시해주었습니다.
위치 데이터를 RDBMS에 저장하였다면 부하 적인 문제도 있을 수 있고 비용 낭비도 있을 수 있습니다.
하지만 Redis를 사용하였을 때는 속도적인 문제나 비용적인 측면에서 많은 효과를 보았던 예입니다.
예제 소스는 아래에서 받을 수 있습니다.
https://github.com/roopy1210/spring-data-redis