5 분 소요

개요

스크린샷 2022-09-29 오전 12 01 54
user-microservice는 UI는 없이 REST API 형식으로 구현할 것이다.
그리고 위 사진에서 보이는 기능들을 개발할 것이다.

APIs

스크린샷 2022-09-29 오전 12 04 31
API Gateway 사용 시에는 uri에 /user-service라는 prefix가 붙어야하며, 모든 마이크로 서비스에 대해 같은 포트(ex.8000)로 호출할 수 있다.
미사용 시 prefix 없이 호출하고자하는 end point만 입력하면 되고, 해당하는 마이크로 서비스의 포트(ex.8081)로 호출해야 한다.

Users Microservice - 기본적인 사용자 가입과 전체 회원 목록 조회

1. 프로젝트 생성

  • Dependencies
    - DevTools, Lombok, Spring Web, Eureka Discovery Client

실행

서비스 디스커버리(= 유레카 서버) 실행

인텔리제이에서 실행하는 것 자체가 부하가 크기 때문에 터미널로 실행하자.
먼저, 터미널 상에서 유레카 서버가 있는 곳으로 이동하자.

$ cd service-discovery
$ mvn spring-boot:run # 유레카 서버 실행


user-service 실행

터미널을 이용해 유레카 서버를 먼저 실행한 뒤,
인텔리제이에서 user-service를 실행한다.
그 다음, http://localhost:8761에 접속하면 아래와 같이 서비스 디스커버리에 user-service가 등록된 것을 확인할 수 있다.
스크린샷 2022-09-29 오전 12 54 09

해당 url을 클릭하여 포트번호를 확인한 뒤, end point를 /health-check로 바꾼다.
참고로, 마이크로 서비스를 재시동 할 때 마다 포트번호는 바뀐다.

2. welcome() 메서드 생성

이번에는 화면에 간단한 인사말을 띄워볼 것이다.
application.yml에 인사말을 정의할건데, 이를 가져오는 방법에는 두 가지가 있다.
우선 application.yml의 맨 아래에 아래 코드를 추가하자.

greeting:
  message: Welcome to the Simple E-commerce.

방법 1. Environment 이용

controller/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");
    }
}

방법 2. @Value 이용

vo/Greeting.java

@Component
@Data
public class Greeting {

    @Value("${greeting.message}")
    private String message;
}

controller/UserController.java

@RestController
@RequestMapping("/")
public class UserController {

    @Autowired // 이처럼 직접 주입 받는 것은 권장하지 않는다.
    private Greeting greeting;

    @GetMapping("welcome")
    public String welcome() {
        return greeting.getMessage();
    }
}

3. H2 Database 연동

pom.xml

dependency의 정확한 이름을 모른다면 mvnrepository.com에 접속하여 검색하자.

 <!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.3.176</version>
    <scope>runtime</scope>
</dependency>

scope을 test로 하면 실행했을 때 결과를 알 수 없기때문에 runtime으로 변경했다.

application.yml

아래와 같이 spring 하위에 h2 관련 내용을 추가한다.

spring:
  application:
    name: user-service
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
#    username: sa
#    password: 1234



h2 console 접속

서비스 디스커버리를 실행한 뒤, user-service를 실행한다.
http://localhost:8761에 접속하여 기동되고 user-service의 주소에 접속하면 Whitelabel Error Page가 뜰텐데,
이 때 접속된 url의 포트번호 이후 엔트포인트를 /h2-console로 변경하면 접속 가능하다.
JDBC URL에는 jdbc:h2:mem:testdb를 입력한다.

Test Connection을 눌렀을 때 아래와 같은 오류가 발생한다면?
스크린샷 2022-09-29 오전 9 06 34
h2의 버전을 1.3.x 으로 변경한다.
스크린샷 2022-09-29 오전 9 09 26
테스트에 성공했다. 이제 Connect를 클릭해 접속할 수 있다.

4. 회원 가입

  • RequestUser: 사용자가 입력한 JSON 데이터를 받는다.
  • UserDto : 데이터가 다른쪽에 있는 서비스 혹은 메서드로 전달되기 위해 사용한다.
  • UserEntity : jpa에서 데이터베이스와 매핑될 수 있는 객체이다.

회원가입에서는 아래 사진에서처럼 RequestUser -> UserDto -> UserEntity 순서로 변환하여 사용할 것이다. 스크린샷 2022-09-29 오전 10 13 38

🗂 vo

vo/RequestUser.java 사용자가 회원가입 할 때 전달하는 정보가 저장되는 클래스이다.

@Data
public class RequestUser {

    @NotNull(message = "Email cannot be null")
    @Size(min = 2, message = "Email not be less than two characters")
    private String email;

    @NotNull(message = "Name cannot be null")
    @Size(min = 2, message = "Name not be less than two characters")
    private String name;

    @NotNull(message = "Password cannot be null")
    @Size(min = 8, message = "Password must be equal or greater than 8 characters")
    private String pwd;
}

🗂 dto

dto/UserDto.java 넘겨받은 RequestUser 값을 데이터베이스에 저장하고 다른쪽으로 이동하기 위한 용도로 dto 클래스를 생성한다.
UserDto에는 사용자가 회원가입 할 때 입력한 정보 뿐만 아니라 랜덤으로 생성된 userId와 생성 날짜인 createdAt 그리고 암호화 된 패스워드인 encryptedPwd가 추가되었다.

@Data
public class UserDto {

    private String email;
    private String name;
    private String pwd;
    private String userId;
    private Date createdAt;
    private String encryptedPwd;
}

🗂 jpa

jpa/UserEntity.java entity에는 validation을 추가할 수 있다.

@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;
}

jpa/UserRepository.java

public interface UserRepository extends CrudRepository<UserEntity, Long> {
}

🗂 service

service/UserService.java

public interface UserService {

    UserDto createUser(UserDto userDto);
}

service/UserServiceImpl.java
UserServiceImpl 클래스에서는 ModelMapper를 이용해 하나의 객체를 다른 객체로 변환한다.

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDto createUser(UserDto userDto) {

        userDto.setUserId(UUID.randomUUID().toString());

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        UserEntity userEntity = mapper.map(userDto, UserEntity.class); // ModelMapper를 이용한 객체 변환

        userEntity.setEncryptedPwd("encrypted_password");

        userRepository.save(userEntity);

        UserDto returnUserDto = mapper.map(userEntity, UserDto.class); // ModelMapper를 이용한 객체 변환

        return returnUserDto;
    }
}

🗂 controller

controller/UserController.java

@RestController
@RequestMapping("/")
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/users")
    public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser requestUser) {
        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

        UserDto userDto = mapper.map(requestUser, UserDto.class);
        userService.createUser(userDto);

        ResponseUser responseUser = mapper.map(userDto, ResponseUser.class);

        return ResponseEntity.status(HttpStatus.CREATED).body(responseUser);
    }
}

테스트

포스트맨

유레카 서버에서 기동되고 있는 서비스의 포트 번호를 확인한 뒤 진행한다.
스크린샷 2022-09-29 오전 10 03 26

h2 console 접속

유레카 서버에서 기동되고 있는 서비스의 url을 클릭한 뒤 엔드포인트를 /h2-console로 변경해 콘솔에 접속할 수 있다.
스크린샷 2022-09-29 오전 10 04 26

5. Spring Security 연동

  • Authentication(인증) + Authorization(권한)
  1. 애플리케이션에 spring security jar을 Dependency에 추가
  2. WebSecurityConfigurerAdapter를 상속받는 Security Configuration 클래스 생성
  3. Security Configuration 클래스에 @EnableWebSecurity 추가
  4. Authentication -> configure(AuthenticationManagerBuilder auth) 메서드를 재정의
  5. Password encode를 위한 BCryptPasswordEncoder 빈 정의
  6. Authorization -> configure(HttpSecurity http) 메서드를 재정의

dependency

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

🗂 security

security/WebSecurity.java

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
            .antMatchers("/users/**")
            .permitAll();
        http.headers().frameOptions().disable();
    }
}

이때, 맨 마지막에 http.headers().frameOptions().disable();를 추가하지 않으면 db 접속시에 프레임이 깨지는 에러가 발생한다.

🗂 service

service/UserServiceImpl.java UserRepository는 빈으로 등록을 시킬건데, BCryptPasswordEncoder는 어디에서도 빈으로 주입한 적이 없다.
따라서 “Could not autowire” 라는 에러메시지가 표시될 것이다.
이를 해결하기 위해서는 가장 먼저 실행되는 스프링 어플리케이션의 기동 클래스 안에 해당하는 빈을 넣어놓으면 된다.

@Service
public class UserServiceImpl implements UserService {

    UserRepository userRepository;
    BCryptPasswordEncoder passwordEncoder;

    @Autowired
    public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) { // 🌟 이때, passwordEmcoder에 에러가 표시될 것이다.
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDto createUser(UserDto userDto) {

        userDto.setUserId(UUID.randomUUID().toString());

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        UserEntity userEntity = mapper.map(userDto, UserEntity.class);

        userEntity.setEncryptedPwd("encrypted_password");

        userRepository.save(userEntity);

        UserDto returnUserDto = mapper.map(userEntity, UserDto.class);

        return returnUserDto;
    }
}

🌟 UserServiceApplication

BCryptPasswordEncoder를 빈으로 등록한다. (메서드 이름은 아무거나 상관없다.)

@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }

    @Bean // 🌟 빈 등록
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

테스트

이전과 같이 포스트맨으로 요청한 뒤, h2 console에서 확인해보면,
아래와 같이 비밀번호가 암호화되어 저장된 모습을 확인할 수 있다.
스크린샷 2022-09-29 오전 11 06 08



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기