티스토리 뷰

회원가입이 로그인보다 간단하므로 회원가입부터 구현한다. 

이메일과 비밀번호를 입력받아 회원가입을 진행한다. 이때, 비밀번호를 db에 저장할 때 암호화해서 저장해야 한다. 그리고, 이미 존재하는 회원이면 에러코드를 출력한다. 

1. 의존성 추가

build.gradle

	implementation 'org.springframework.boot:spring-boot-starter-security'

	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

2. SecurityConfig.java 작성

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encodePassword() { /*비밀번호 암호화*/
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .formLogin().disable()
                .httpBasic().disable()
                .headers().frameOptions().sameOrigin()
                .and()
                .authorizeRequests()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/swagger-ui/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/v3/api-docs").permitAll()
                .antMatchers("/user/**").permitAll()
                /*위의 api는 무조건적인 접근 허용 */
                .anyRequest().authenticated(); /*그 외는 인증된 사용자에 한해 접근 허용*/
    }

}

 

3. User.java 엔티티 작성

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;

    private String email;

    private String password;

    @Enumerated(EnumType.STRING)
    private UserConstants.Role role;

    public void encryptPassword(PasswordEncoder passwordEncoder) {
        password = passwordEncoder.encode(password);
    }

}

 

4. 레포지토리 작성

UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {

}

UserRepositoryCustom.java

public interface UserRepositoryCustom {
    Optional<User> findByEmail(String email);
}

UserRepositoryImpl.java

public class UserRepositoryImpl implements UserRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    public UserRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public Optional<User> findByEmail(String email) {
        return Optional.ofNullable(queryFactory.selectFrom(user)
                .where(user.email.eq(email))
                .fetchFirst());
    }
}

 

 

5. 서비스 작성

UserService.java

public interface UserService {
    SignupResponse singup(SignupRequest signupRequest);
    }

UserServiceImpl.java

@RequiredArgsConstructor
@Service
@Slf4j
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtTokenProvider tokenProvider;
    private final RedisTemplate redisTemplate;

    @Override
    public SignupResponse singup(UserDto.SignupRequest signupRequest) {
        this.validateOverlap(signupRequest.getEmail()); //존재하는 회원인지 확인
        User user = userMapper.toEntity(signupRequest); 
        user.encryptPassword(passwordEncoder); //비밀번호 암호화
        user.setRole(ROLE_USER);
        this.userRepository.save(user);
        return new SignupResponse(signupRequest.getEmail());
    }

    private void validateOverlap(String email) {
        userRepository.findByEmail(email)
                .ifPresent((m -> {
                    throw new OverlapUserException();
                }));
    }
}

6. exception 작성 

오류 코드 출력하는 것이 생각보다 까다롭다

UserException.java

public abstract class UserException extends ApplicationException {
    protected UserException(String errorCode, HttpStatus httpStatus, String message) {
        super(errorCode, httpStatus, message);
    }
}

OverlapException.java

public class OverlapUserException extends UserException {

    public OverlapUserException() {
        super(UserConstants.UserExceptionList.OVERLAP_USER.getErrorCode(),
                UserConstants.UserExceptionList.OVERLAP_USER.getHttpStatus(),
                UserConstants.UserExceptionList.OVERLAP_USER.getMessage());
    }
}

ApplicationException.java

public abstract class ApplicationException extends RuntimeException {

    private final String errorCode;
    private final HttpStatus httpStatus;

    protected ApplicationException(String errorCode, HttpStatus httpStatus, String message) {
        super(message);
        this.errorCode = errorCode;
        this.httpStatus = httpStatus;
    }

    public String getErrorCode() {
        return errorCode;
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

GlobalExceptionHandler.java

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final String LOG_FORMAT = "Class : {}, Code : {}, Message : {}";


    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity<ApiErrorResponse> applicationException(ApplicationException e) {
        String errorCode = e.getErrorCode();
        log.warn(
                LOG_FORMAT,
                e.getClass().getSimpleName(),
                errorCode,
                e.getMessage()
        );
        return ResponseEntity
                .status(e.getHttpStatus())
                .body(new ApiErrorResponse(errorCode, e.getMessage()));
    }
 }

상속 관계를 정리하면 아래 그림과 같다.

 

최종적으로 ApplicationException을 GlobalExceptionHandler에서 처리해 OverlapUserException인 경우 errorcode와 errormessage를 ResponseEntity로 감싸 객체로 반환해준다. 

7. UserDto 작성 

public abstract class UserDto {

    @Getter
    @AllArgsConstructor
    @Builder
    @ApiModel(description = "회원가입을 위한 요청객체")
    public static class SignupRequest {
        @NotBlank(message = "이메일을 입력해주세요")
        @ApiModelProperty(notes = "이메일을 입력해주세요")
        private String email;

        @NotBlank(message = "비밀번호를 입력해주세요")
        @ApiModelProperty(notes = "비밀번호를 입력해주세요")
        private String password;

    }

    @Getter
    @Builder
    @ApiModel(description = "회원가입을 위한 응답객체")
    public static class SignupResponse {
        private String email;

        @QueryProjection
        public SignupResponse(String email) {
            this.email = email;
        }
    }
 }

8. UserController 작성

@RequiredArgsConstructor
@RestController
@RequestMapping("user")
@Api(tags = "User API")
public class UserController {
    private final UserService userService;

    @ApiOperation(value = "회원가입", notes = "회원가입을 합니다.")
    @PostMapping("/signup")
    public ResponseEntity<ResponseDto<SignupResponse>> singupUser(@Valid @ModelAttribute SignupRequest signupRequest) {
        return ResponseEntity.ok(ResponseDto.create(UserConstants.EBoardResponseMessage.SIGNUP_SUCCESS.getMessage(), this.userService.singup(signupRequest)));
    }
}