이 글은 인프런 Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의를 들은 후에 정리한 글입니다.
(https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard)
이 글에서 설명되는 내용들은 모두 실제 강의와 다른 Spring Boot 3.1.2 버전입니다.
Users Microservice 개요
개발할 Users Microservice의 전체적인 개요에 대해 살펴보자.
Users Microservice의 대략적인 기능은 다음과 같다.
- 신규 회원 등록
- 회원 로그인
- 상세 정보 확인
- 회원 정보 수정/삭제
- 상품 주문
- 주문 내역 확인
UI 없이 JSON 기반의 API만 구현할 것이다.
아래는 API Gateway 사용 유무에 따른 URI 내용의 차이이다.
기능 | URI (API Gateway 사용) | URI (API Gateway 사용 ❌) | HTTP Method |
사용자 정보 등록 | /user-service/users | /users | POST |
전체 사용자 조회 | /user-service/users | /users | GET |
사용자 정보, 주문 내역 조회 | /user-service/users/{user_id} | /users/{user_id} | GET |
작동 상태 확인 | /user-service/users/health_check | /users/health_check | GET |
환영 메시지 | /user-service/users/welcome | /users/welcome | GET |
Gateway를 사용하면 prefix와 엔드포인트명이 필요할 것이고, 그렇지 않다면 prefix 없이 엔드포인트명만 입력하면 된다.
Users Microservice - 프로젝트 생성
프로젝트를 어떤 과정을 거쳐서 개발할 것인지에 대한 설명이다. 이어서 작업을 진행할 것이므로 여기서는 설명은 굳이 하지 않겠다.
Users Microservice - welcome() 메서드
기존에 만들었던 user-service를 그대로 사용해서 개발한다.
main 함수에 @EnableDiscoveryClient가 이미 붙어있고, application.yml 포트 설정과 instace-id 설정도 이미 완료되어 있을 것이다. 혹시 설정이 되어 있지 않다면, 이전 강의를 다시 듣고 오자.
컨트롤러 클래스를 만들어보자.
// UserController.java
@RestController
@RequestMapping("/")
public class UserController {
@GetMapping("/health_check")
public String status() {
return "It's Working in User Service.";
}
}
터미널에서 discovery-service를 구동한 후 브라우저에서 잘 실행되는지 확인한다.
Eureka 서버가 잘 구동되었음을 확인한 후에 user-service를 구동한다.
아래처럼 랜덤으로 부여된 57539 포트에서 잘 실행됨을 브라우저에서 확인할 수 있다.
application.yml에 아래와 같이 임의의 값을 추가한다.
# application.yml
...
greeting:
message: Welcome to the Simple E-Commerce.
application.yml에 추가한 임의의 값을 출력하기 위한 메서드를 컨트롤러에 추가한다.
// UserController.java
@RestController
@RequestMapping("/")
public class UserController {
private Environment env;
@Autowired
public UserController(Environment env) {
this.env = env;
}
...
@GetMapping("/welcome")
public String welcome() {
return env.getProperty("greeting.message");
}
}
이제 브라우저에서 의도한 값이 잘 출력되는지 확인한다.
@Value라는 어노테이션을 사용해서도 application.yml의 값을 불러올 수도 있다.
먼저 vo 패키지 안에 Greeting 정보를 저장할 객체를 하나 생성해 준다.
// Greeting.java
@Component
@Data
public class Greeting {
@Value("${greeting.message}")
private String message;
}
이제 이 객체를 사용하여 Yaml 파일에 저장된 정보를 불러온다.
// UserController.java
@RestController
@RequestMapping("/")
public class UserController {
...
@Autowired
private Greeting greeting;
...
@GetMapping("/welcome")
public String welcome() {
// return env.getProperty("greeting.message");
return greeting.getMessage();
}
}
서비스를 다시 재시작한 후 확인해 보면, 위와 같이 @Value를 사용하더라도, 브라우저에서 동일하게 출력됨을 확인할 수 있다.
※ VO가 무엇일까? 헷갈리는 다른 개념들과 함께 정리해보았다.
Users Microservice - H2 데이터베이스 연동
mvnRepository에서 H2 DB의 의존성을 찾아 다음과 같이 pom.xml에 추가한다.
<!-- pom.xml -->
...
<dependencies>
...
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.220</version>
<scope>runtime</scope>
</dependency>
</dependencies>
application.yml에도 h2 DB 관련 설정을 추가한다.
# application.yml
...
spring:
application:
name: user-service
h2:
console:
enabled: true
settings:
web-allow-others: true
path: /h2-console
...
이러면 다음과 같은 오류가 발생하며 접속이 안된다.
그래서 아래 글을 참고하여 변경하였다. (아래 파트의 JPA를 사용한 다른 방법 참고하여 데이터베이스를 만들고 시작하는 것이 더 편해 보인다...)
테스트 접속에 성공하였다.
Users Microservice - 사용자 추가
이제부턴 회원가입 서비스를 위한 사용자 추가 기능을 구현해 보겠다.
클라이언트가 회원가입을 위해 이메일, 이름, 비밀번호를 입력할 것이다. 입력받은 회원 가입 정보들을 가지고 requestUser라는 VO 클래스를 만들고, Validation Check를 해줄 것이다.
"null값을 입력할 수 없음", "최소 몇 글자 이상을 입력해야 함"라는 조건을 Validation Check 시에 확인한다.
클라이언트가 회원 가입 정보를 JSON 타입으로 전달하면 비즈니스 로직에 의해 처리가 된다.
Users Microservice - JPA ➀
강의에선 validation 의존성을 추가하라는 내용이 없는 것 같은데, 어쨌든 아래의 의존성을 pom.xml에 추가하여야 추후에 사용할 @NotNull, @Size 등을 사용할 수 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
vo 패키지에 회원가입을 위한 정보를 받을 RequestUser 클래스를 생성한다.
// RequestUser.java
@Data
public class RequestUser {
@NotNull(message = "Email can't be null")
@Size(min = 2, message = "Email not be less than two characters")
@Email
private String email;
@NotNull(message = "Name can't be null")
@Size(min = 2, message = "Name not be less than two characters")
private String name;
@NotNull(message = "Password can't be null")
@Size(min = 8, message = "Password must be equal or greater than two characters")
private String pwd;
}
dto 패키지 내에 UserDto 클래스를 만든다.
// UserDto.java
@Data
public class UserDto {
private String email;
private String name;
private String pwd;
private String userId;
private Date createdAt;
private String encryptedPwd;
}
서비스 로직을 처리하기 위한 서비스 계층을 만들어보자.
먼저 service 패키지 내에 서비스 계층의 인터페이스인 UserService.java를 먼저 생성한다.
// UserService.java
public interface UserService {
UserDto createUser(UserDto userDto);
}
그 후 인터페이스를 구현하는 UserServiceImpl 클래스를 생성하고, 인터페이스의 메서드를 아래와 같이 구현하도록 한다.
아직 세부 메서드는 구현이 완료된 상태가 아니다.
// UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
return null;
}
}
서비스 메서드 생성을 완료하였으므로, 이제 User 엔티티를 생성해 보자.
먼저, JPA 사용을 위해 의존성을 pom.xml에 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
다음으로 UserEntity를 생성한다.
// UserEntity.java
@Data
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String email;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String userId;
@Column(nullable = false, unique = true)
private String encryptedPwd;
}
엔티티까지 생성이 완료되었으므로, 유저 관련 Repository를 생성한다.
// UserRepository.java
public interface UserRepository extends CrudRepository<UserEntity, Long> {
}
※ 보통 프로젝트를 개발할 때는 CrudRepository가 아닌 JpaRepository를 사용했는데 이 둘의 차이가 뭘까?
위의 그림을 보면 Repository Interface를 CrudRepository, JpaRepository 둘 다 상속하는 것으로 확인할 수 있다.
CrudRepository는 단순히 CRUD(CREATE, READ, UPDATE, READ) 관련 기능을 사용하는 경우를 위한 것이고, JpaRepository는 CrudRepository와는 다르게 PagingAndSortingRepository를 상속하기에 페이징과 Sorting와 같은 JPA의 기능을 사용하는 경우를 위한 것이다.
JPA 관련 클래스 생성이 완료가 되었으므로, 미완성된 서비스 로직을 완성해 보자.
하나의 클래스가 가지고 있는 정보들을 다른 클래스로 변환시켜 줄 수 있는 ModelMapper를 위한 의존성을 먼저 추가해 주자.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.8</version>
</dependency>
다음으로 UserServiceImpl에서 미완성된 로직을 아래와 같이 완성시킨다.
// UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper(); // ModelMapper 선언
// ModelMapper가 매칭할 수 있는 정보를 Strict로 설정
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = mapper.map(userDto, UserEntity.class); // ModelMapper를 사용해 변환 작업 수행
userEntity.setEncryptedPwd("encrypted_password"); // EncryptedPwd는 추후 구현
userRepository.save(userEntity);
return null;
}
}
이제 컨트롤러에서 위의 서비스 로직을 사용할 createUser 메서드를 생성하도록 하자.
@RestController
@RequestMapping("/")
public class UserController {
private Environment env;
private final UserService userService;
@Autowired
public UserController(Environment env, UserService userService) {
this.env = env;
this.userService = userService;
}
...
@PostMapping("/users")
public String createUser(@RequestBody RequestUser user){
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = mapper.map(user, UserDto.class);
userService.createUser(userDto);
return "Create user method is called";
}
}
Users Microservice - JPA ➁
위에서 만든 회원 가입을 위한 유저 추가 기능이 잘 작동되는지 Postman을 사용해 확인해 보자.
다음과 같이 기능이 잘 작동함을 확인할 수 있다.
이를 h2 DB에서 확인해 보면, 충격적 이게도 테이블이 생성된 것을 확인할 수 없다.
무엇이 문제인지 한참을 고민해 봐도 해결되지 않아 아래 블로그 글을 참조하여 아예 application.yml 설정을 바꾸니까 해결이 되었다.
아마도 yml 파일에서의 들어 쓰기 단계에서 오류가 난 것 같다....
# application.yml
...
spring:
application:
name: user-service
h2:
console:
enabled: true
settings:
web-allow-others: true
path: /h2-console
datasource:
url: jdbc:h2:mem:testdb
username: sa
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
...
다시 h2 DB에 접속해 보면, 데이터가 잘 입력된 것을 확인할 수 있다.
CREATE Success의 상태코드는 200이 아닌 201이므로 UserController에서 ResponseEntity를 사용하여 201 상태코드를 반환하게 변경해 보자.
// UserController.java
@PostMapping("/users")
public ResponseEntity createUser(@RequestBody RequestUser user){
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = mapper.map(user, UserDto.class);
userService.createUser(userDto);
return new ResponseEntity(HttpStatus.CREATED);
}
아래와 같이 잘 변경된 것을 확인할 수 있다.
이제 API의 반환값을 주도록 하자.
Controller 계층을 다음과 같이 변경한다.
// UserController.java
@PostMapping("/users")
public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser user){
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = mapper.map(user, UserDto.class);
userService.createUser(userDto);
ResponseUser responseUser = mapper.map(userDto, ResponseUser.class);
return ResponseEntity.status(HttpStatus.CREATED).body(responseUser);
}
다음으로 Service 계층에서 비즈니스 로직을 변경한다.
// UserServiceImpl.java
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper(); // ModelMapper 선언
// ModelMapper가 매칭할 수 있는 정보를 Strict로 설정
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = mapper.map(userDto, UserEntity.class); // ModelMapper를 사용해 변환 작업 수행
userEntity.setEncryptedPwd("encrypted_password"); // EncryptedPwd는 추후 구현
userRepository.save(userEntity);
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
의도한 대로 Dto를 반환하는 것을 확인할 수 있다.
Users Microservice - Spring Security 연동
이제 Spring Security를 사용하여, 인증과 인가 기능을 추가할 것이다.
먼저 pom.xml에 Spring Security 의존성을 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
security 패키지를 만들고, WebSecurity 클래스를 생성하면 된다.
강의 내용은 이미 사라진 메서드들을 활용하고 있기에 사용할 수 없었고, 같이 스터디를 진행하는 팀원의 코드를 가져와 사용해보았다.
하지만 아래와 같은 오류가 발생했다.
If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).
문제는 AntPathRequestMatcher를 사용하지 않아서 오류가 발생하였던 것이다.
그래서 최종적으로 아래와 같이 변경하였더니 잘 실행이 되었다.
// WebSecurity.java
@Configuration
@EnableWebSecurity
public class WebSecurity {
// 변경 부분
private static final AntPathRequestMatcher[] WHITE_LIST={
new AntPathRequestMatcher("/users/**"),
new AntPathRequestMatcher("/"),
new AntPathRequestMatcher("/**")
};
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.csrf(csrf->csrf.disable());
http.authorizeHttpRequests((auth)-> auth
.requestMatchers(WHITE_LIST).permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated());
http.headers(h->h.frameOptions(f->f.disable()).disable());
return http.build();
}
}
이제 암호화된 Password를 지정해보자.
UserService를 아래와 같이 변경한다.
// UserService.java
@Service
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Autowired
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
...
}
BCryptPasswordEncoder는 생성한 적이 없기 때문에 자동 빈 주입이 불가능하다.
그렇기에 실행 시에 해당 빈이 존재하도록 변경해야한다.
이를 위해 아래와 같이 main 함수가 존재하는 UserServiceApplication에 빈을 생성해주도록 하자.
// UserServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
이제 생성한 빈을 Service 계층에서 직접 사용해보자.
passwordEncoder의 encode 메서드를 활용하여 비밀번호를 암호화 할 수 있다.
// UserServiceImpl.java
...
@Override
public UserDto createUser(UserDto userDto) {
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper(); // ModelMapper 선언
// ModelMapper가 매칭할 수 있는 정보를 Strict로 설정
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = mapper.map(userDto, UserEntity.class); // ModelMapper를 사용해 변환 작업 수행
userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd())); // 변경된 부분: 비밀번호 암호화
userRepository.save(userEntity);
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
...
실행해보면 비밀번호가 아래와 같이 잘 암호화 된 것을 확인할 수 있다.
같은 비밀번호를 사용해서 다른 회원으로 가입을 하더라도, 암호화 처리된 비밀번호는 다름을 확인할 수 있을 것이다.
만약 암호화 처리된 값이 동일하다면, 암호화를 한 의미가 없게 된다.
다른 스터디원께서 암호화의 원리에 대해 잘 정리하셔서 이를 참고하여 다시 정리해보면,
암호화된 패스워드를 확인해보면, $를 기준으로 세 부분으로 나눌 수 있다.
첫번째는 BCryptPasswordEncoder의 버전이고, 두번째는 해시함수 적용 횟수이다.
마지막 부분은 Salt값과 암호화된 결과이다. 암호화할 패스워드에 Salt라는 랜덤값을 붙여서 10번 해시함수를 적용한 결과값이다.
원래 Salt값은 공개되어 있는데, 이마저도 숨기는 것을 Pepper라고 한다. 관련하여 자세한 내용은 위의 블로그를 참고하시길...