이번 문서 링크는 아래와 같습니다!
가장 아래로 내려보시면 문서에 대한 요약을 함께 적어놨습니다 참고해주세요 :)
https://redis.io/learn/develop/java/redis-and-spring-course/lesson_4
User Roles & Secondary Indexes
As we learned in the previous lesson, the @Indexed annotation can be used to create a secondary index. Secondary indexes enable lookup operations based on native Redis structures. The index is maintained on every save/update of an indexed object. To add a
redis.io
사용자 역할 및 보조 인덱스
저자: Brian Sam-Bodden, Redis 개발자 옹호자
목표
사용자-역할 도메인을 완성하고, JSON 데이터를 로드 및 변환하며, Redi2Read API 작성을 시작합니다.
학습 내용
- Jackson을 사용하여 JSON 데이터를 로드하는 방법
- 보조 인덱스를 생성하고 사용하는 방법
- 리포지토리를 REST 컨트롤러와 함께 사용하는 방법
문제 발생 시:
- 이 레슨의 진행 상황은 redi2read GitHub 저장소에서 확인할 수 있습니다.
사용자 로드하기
이제 역할을 생성했으니, 제공된 JSON 데이터를 사용하여 사용자를 로드하겠습니다. src/main/resources/data/users/users.json
파일에는 다음과 같은 배열 형태의 JSON 사용자 객체가 포함되어 있습니다:
{
"password": "9yNvIO4GLBdboI",
"name": "Georgia Spencer",
"id": -5035019007718357598,
"email": "georgia.spencer@example.com"
}
JSON 필드는 User POJO 속성의 JavaBean 이름과 정확히 일치합니다.
사용자 리포지토리
먼저 UserRepository를 생성합니다. RoleRepository와 마찬가지로 CrudRepository를 확장합니다. src/main/java/com/redislabs/edu/redi2read/repositories
디렉토리에 UserRepository 인터페이스를 다음과 같이 생성합니다:
package com.redislabs.edu.redi2read.repositories;
import com.redislabs.edu.redi2read.models.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CrudRepository<User, String> {
User findFirstByEmail(String email);
}
findFirstByEmail 메서드는 이전에 생성한 User 모델의 email 필드에 대한 인덱스를 활용합니다. Spring Repository는 런타임에 finder 메서드의 구현을 제공합니다.
이제 boot 패키지 아래에 또 다른 CommandLineRunner를 생성하여 사용자를 로드합니다. Roles와 유사한 방법을 따르되, 디스크에서 JSON 데이터를 로드하고 가장 인기 있는 Java JSON 라이브러리 중 하나인 Jackson을 사용합니다.
전제 조건
사용자 로드 레시피를 기반으로 애플리케이션에서 필요하지만 현재 할 수 없는 두 가지가 있습니다:
- 일반 텍스트 사용자 비밀번호를 인코딩하는 방법
- 역할을 이름으로 찾는 방법
비밀번호 인코딩
PasswordEncoder 구현은 BCrypt 강력 해싱 함수를 사용합니다. Redi2readApplication
클래스에 다음을 추가합니다:
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
적절한 import를 추가합니다:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
보조 인덱스: 이름으로 역할 찾기
이전 레슨에서 배운 것처럼, @Indexed 주석을 사용하여 보조 인덱스를 생성할 수 있습니다. 보조 인덱스를 사용하면 기본 Redis 구조를 기반으로 조회 작업을 수행할 수 있습니다. 인덱스는 인덱싱된 객체가 저장/업데이트될 때마다 유지됩니다. Role 모델에 보조 인덱스를 추가하려면 @Indexed 주석을 추가합니다:
@Data
@Builder
@RedisHash
public class Role {
@Id
private String id;
@Indexed
private String name;
}
해당 import를 추가합니다:
import org.springframework.data.redis.core.index.Indexed;
이제 새로운 Role 인스턴스가 생성되면, Spring Data Redis는 다음을 수행합니다:
- "이름별" 인덱스 생성: Redis Set으로 생성된 키는
com.redislabs.edu.redi2read.models.Role:name:superuser
이고, 인덱싱된 객체의 ID인 "abc-123"을 포함합니다. - "superuser" 역할의 인덱스 목록: Redis Set으로 생성된 키는
com.redislabs.edu.redi2read.models.Role:abc-123:idx
이고, 인덱스 키인com.redislabs.edu.redi2read.models.Role:name:superuser
를 포함합니다.
이미 생성된 역할을 인덱싱하려면 다시 저장하거나 재생성해야 합니다. 이미 역할을 자동으로 시딩했기 때문에 Redis CLI와 DEL 명령을 사용하여 삭제하고 서버를 다시 시작할 수 있습니다:
127.0.0.1:6379> KEYS com.redislabs.edu.redi2read.models.Role*
1) "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464"
2) "com.redislabs.edu.redi2read.models.Role:9d383baf-35a0-4d20-8296-eedc4bea134a"
3) "com.redislabs.edu.redi2read.models.Role"
127.0.0.1:6379> DEL "com.redislabs.edu.redi2read.models.Role:c4219654-0b79-4ee6-b928-cb75909c4464" "com.redislabs.edu.redi2read.models.Role:9d383baf-35a0-4d20-8296-eedc4bea134a" "com.redislabs.edu.redi2read.models.Role"
(integer) 3
127.0.0.1:6379>
RoleRepository에 finder 메서드를 추가합니다:
@Repository
public interface RoleRepository extends CrudRepository<Role, String> {
Role findFirstByName(String role);
}
CreateUsers CommandLineRunner
src/main/java/com/redislabs/edu/redi2read/boot
디렉토리에 CreateUsers.java 파일을 다음 내용으로 생성합니다:
package com.redislabs.edu.redi2read.boot;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.redislabs.edu.redi2read.models.Role;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.RoleRepository;
import com.redislabs.edu.redi2read.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(2)
@Slf4j
public class CreateUsers implements CommandLineRunner {
@Autowired
private RoleRepository roleRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
if (userRepository.count() == 0) {
// load the roles
Role admin = roleRepository.findFirstByName("admin");
Role customer = roleRepository.findFirstByName("customer");
try {
// create a Jackson object mapper
ObjectMapper mapper = new ObjectMapper();
// create a type definition to convert the array of JSON into a List of Users
TypeReference<List<User>> typeReference = new TypeReference<List<User>>() {};
// make the JSON data available as an input stream
InputStream inputStream = getClass().getResourceAsStream("/data/users/users.json");
// convert the JSON to objects
List<User> users = mapper.readValue(inputStream, typeReference);
users.stream().forEach((user) -> {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.addRole(customer);
userRepository.save(user);
});
log.info(">>>> " + users.size() + " Users Saved!");
} catch (IOException e) {
log.info(">>>> Unable to import users: " + e.getMessage());
}
User adminUser = new User();
adminUser.setName("Adminus Admistradore");
adminUser.setEmail("admin@example.com");
adminUser.setPassword(passwordEncoder.encode("Reindeer Flotilla"));
adminUser.addRole(admin);
userRepository.save(adminUser);
log.info(">>>> Loaded User Data and Created users...");
}
}
}
설명
- @Autowired 주석을 사용하여 RoleRepository, UserRepository, BCryptPasswordEncoder를 주입합니다.
- CreateRoles CommandLineRunner와 마찬가지로, 데이터베이스에 사용자가 없을 경우에만 로직을 실행합니다.
- Repository 커스텀 finder 메서드 findFirstByName을 사용하여 admin과 customer 역할을 로드합니다.
- Jackson ObjectMapper와 TypeReference를 생성하여 JSON을 Java 객체로 직렬화합니다.
- Class 객체에서 getResourceAsStream을 사용하여 리소스 디렉토리에서 JSON 파일을 로드합니다.
- ObjectMapper를 사용하여 입력 스트림을 User 객체 목록으로 변환합니다.
- 각 사용자에 대해 비밀번호를 인코딩하고 customer 역할을 추가합니다.
- 파일 끝 부분에서는 admin 역할이 있는 단일 사용자를 생성합니다.
애플리케이션 재시작 시 다음과 같은 출력이
표시됩니다:
2021-04-03 10:05:04.222 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.Redi2readApplication : Started Redi2readApplication in 2.192 seconds (JVM running for 2.584)
2021-04-03 10:05:04.539 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateRoles : >>>> Created admin and customer roles...
2021-04-03 10:06:27.292 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateUsers : >>>> 1000 Users Saved!
2021-04-03 10:06:27.373 INFO 40386 --- [ restartedMain] c.r.edu.redi2read.boot.CreateUsers : >>>> Loaded User Data and Created users...
로드된 사용자 탐색하기
Redis CLI를 사용하여 데이터를 탐색합니다:
127.0.0.1:6379> KEYS "com.redislabs.edu.redi2read.models.User"
1) "com.redislabs.edu.redi2read.models.User"
127.0.0.1:6379> TYPE "com.redislabs.edu.redi2read.models.User"
set
127.0.0.1:6379> SCARD "com.redislabs.edu.redi2read.models.User"
(integer) 1001
127.0.0.1:6379> SRANDMEMBER "com.redislabs.edu.redi2read.models.User"
"-1848761758049653394"
127.0.0.1:6379> HGETALL "com.redislabs.edu.redi2read.models.User:-1848761758049653394"
1) "id"
2) "-1848761758049653394"
3) "_class"
4) "com.redislabs.edu.redi2read.models.User"
5) "roles.[0]"
6) "com.redislabs.edu.redi2read.models.Role:a9f9609f-c173-4f48-a82d-ca88b0d62d0b"
7) "name"
8) "Janice Garza"
9) "email"
10) "janice.garza@example.com"
11) "password"
12) "$2a$10$/UHTESWIqcl6HZmGpWSUHexNymIgM7rzOsWc4tcgqh6W5OVO4O46."
사용자 키를 포함한 Redis Set이 있고, 해당 Set의 cardinality는 1001입니다. SRANDMEMBER 명령을 사용하여 Set에서 임의의 멤버를 가져오고, 이를 사용하여 특정 사용자 해시 데이터를 가져옵니다.
Redi2Read API 작성하기
이제 사용자와 역할이 있으므로, 사용자 관리 기능을 노출하는 UserController를 생성합니다:
package com.redislabs.edu.redi2read.controllers;
import com.redislabs.edu.redi2read.models.User;
import com.redislabs.edu.redi2read.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping
public Iterable<User> all() {
return userRepository.findAll();
}
}
GET 요청을 사용하여 모든 사용자를 조회할 수 있습니다:
curl --location --request GET 'http://localhost:8080/api/users/'
출력은 JSON 객체 배열 형태입니다:
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "denise.powell@example.com",
"password": "$2a$10$pMJjQ2bFAUGlBTX9cHsx/uGrbbl3JZmmiR.vG5xaVwQodQyLaj52a",
"passwordConfirm": null,
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
},
...
]
비밀번호와 비밀번호 확인 필드를 제외한 응답을 위해 @JsonIgnoreProperties
를 사용하여 User 클래스를 수정합니다:
@JsonIgnoreProperties(value = { "password", "passwordConfirm" }, allowSetters = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
@Data
@RedisHash
public class User {
// ... 기존 코드 ...
}
import 문 추가:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
요청을 다시 실행하면 JSON 응답에 비밀번호 필드가 제외됩니다:
[
{
"id": "-1180251602608130769",
"name": "Denise Powell",
"email": "denise.powell@example.com",
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
},
...
]
UserController에 이메일 주소로 사용자를 조회하는 메서드를 추가합니다:
@GetMapping
public Iterable<User> all(@RequestParam(defaultValue = "") String email) {
if (email.isEmpty()) {
return userRepository.findAll();
} else {
Optional<User> user = Optional.ofNullable(userRepository.findFirstByEmail(email));
return user.isPresent() ? List.of(user.get()) : Collections.emptyList();
}
}
import 문 추가:
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.web.bind.annotation.RequestParam;
curl 명령을 사용하여 엔드포인트를 호출합니다:
curl --location --request GET 'http://localhost:8080/api/users/?email=donald.gibson@example.com'
예상된 결과를 반환합니다:
[
{
"id": "-1266125356844480724",
"name": "Donald Gibson",
"email": "donald.gibson@example.com",
"roles": [
{
"id": "a9f9609f-c173-4f48-a82d-ca88b0d62d0b",
"name": "customer"
}
]
}
]
위 글에서 설명하려는 주요 내용은 다음과 같습니다:
- 사용자-역할 도메인 생성 및 데이터 로드:
- JSON 데이터를 사용하여 사용자와 역할 데이터를 로드하고 변환하는 방법을 설명합니다.
- Jackson 라이브러리를 사용하여 JSON 데이터를 Java 객체로 변환합니다.
- Redis 보조 인덱스 생성:
@Indexed
주석을 사용하여 Redis에서 보조 인덱스를 생성하고 활용하는 방법을 설명합니다.- 보조 인덱스를 통해 특정 필드(예: 이메일)로 사용자를 검색할 수 있습니다.
- 비밀번호 인코딩:
BCryptPasswordEncoder
를 사용하여 사용자 비밀번호를 안전하게 인코딩하는 방법을 설명합니다.
- Redis 리포지토리 생성 및 사용:
CrudRepository
를 확장하여 사용자와 역할에 대한 리포지토리를 생성하고 사용하는 방법을 설명합니다.- 리포지토리를 사용하여 데이터를 저장, 검색, 업데이트하는 방법을 설명합니다.
- Spring REST 컨트롤러 작성:
- Spring REST 컨트롤러를 작성하여 사용자 관리 기능을 노출하는 방법을 설명합니다.
- RESTful API를 통해 모든 사용자 데이터를 조회하고, 특정 이메일로 사용자를 검색하는 방법을 설명합니다.
- 데이터 시딩 및 탐색:
- CommandLineRunner를 사용하여 애플리케이션 시작 시 데이터를 자동으로 시딩하는 방법을 설명합니다.
- Redis CLI를 사용하여 로드된 데이터를 탐색하고 확인하는 방법을 설명합니다.
이 글은 Spring Boot와 Redis를 사용하여 사용자와 역할 도메인을 생성하고 관리하는 방법을 실습 형태로 설명하며, JSON 데이터를 로드하고 보조 인덱스를 활용하여 효율적으로 데이터를 검색하는 방법을 중점적으로 다룹니다.
'Redis > Redis 개념' 카테고리의 다른 글
Redis기본 개념, 자료구조 (2) | 2024.06.12 |
---|---|
[Spring/Redis] Redis 문서 정리(Search With Redis) (0) | 2024.06.11 |
[Spring/Redis] Redis문서 정리(Object Mapping & Redis Repository) (0) | 2024.06.11 |
[Spring/Redis] Redis문서정리(Redis Spring 시작하기) (0) | 2024.06.11 |
[Spring/Redis] Redis문서정리(Redis OM -Spring실습 -Hash) (0) | 2024.06.11 |