SNS 제작 (회원가입)

개발연습생log·2022년 12월 21일
0

SNS 제작

목록 보기
2/15
post-thumbnail

✨ 개요

🏃 목표

📢 회원가입을 구현하자.

📢 요구사항

  • 모든 회원은 회원 가입을 통해 회원이 된다.
  • 회원 가입 성공 시 "회원가입 성공" 을 리턴한다.
  • userName 이 존재할 시 예외처리를 한다.
  • POST /join
  • 입력 폼 (JSON 형식)
    {
    	"userName" : "user1",
    	"password" : "user1234"
    }
  • 리턴 (JSON 형식)
    {
        "resultCode": "SUCCESS",
        "result": {
            "userId": 5,
            "userName": "test1"
        }
    }

📜 접근방법

TO-DO

  • 회원가입 컨트롤러 구현
  • 회원가입 컨트롤러 테스트
  • 유저 리포지토리 구현
  • 회원가입 서비스 구현
  • 예외 처리
  • 패스워드 인코딩 처리

🔧 구현

회원가입 컨트롤러 구현

UserController

<@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/join")
    public ResponseEntity<Response> join(@RequestBody UserJoinRequest userJoinRequest) {
        UserJoinResponse userJoinResponse = userService.join(userJoinRequest.getUserName(), userJoinRequest.getPassword());
        return ResponseEntity.ok().body(Response.of("SUCCESS", userJoinResponse));
    }
}

DTO

  • JoinRequestDTO
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class UserJoinRequest {
    private String userName;
    private String password;
}
  • JoinResponseDTO
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class UserJoinResponse {
    private Long userId;
    private String userName;

    public static UserJoinResponse of(Long userId, String userName) {
        return UserJoinResponse.builder()
                .userId(userId)
                .userName(userName)
                .build();
    }
}
  • ResponseDTO
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class Response<T> {

    private String resultCode;
    private T result;

    public static <T> Response of(String resultCode, T result) {
        return Response.builder()
                .resultCode(resultCode)
                .result(result)
                .build();
    }

}

회원가입 컨트롤러 테스트

의존성 추가

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

UserController

@WebMvcTest(UserController.class)
@MockBean(JpaMetamodelMappingContext.class)
class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    @DisplayName("회원가입 성공")
    void join_SUCCESS() throws Exception {

        String userName = "홍길동";
        String password = "0000";

        mockMvc.perform(post("/api/v1/users/join")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(new UserJoinRequest(userName, password))))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("회원가입 실패_userName이 중복인 경우")
    void join_FAILED() throws Exception {

        String userName = "홍길동";
        String password = "0000";

        when(userService.join(any(),any()))
                .thenThrow(new AppException(ErrorCode.DUPLICATED_USER_NAME,ErrorCode.DUPLICATED_USER_NAME.getMessage()));

        mockMvc.perform(post("/api/v1/users/join")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(new UserJoinRequest(userName, password))))
                .andDo(print())
                .andExpect(status().isConflict());
    }
}

UserRepository 구현

엔티티

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

    private String userName;
    private String password;

    public static User of(String userName, String password) {
        return User.builder()
                .userName(userName)
                .password(password)
                .build();
    }
}

UserRepository

public interface UserRepository extends JpaRepository<User,Long> {
    Optional<User> findByUserName(String userName);
}

회원가입 서비스 구현

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;
    public UserJoinResponse join(String userName, String password) {
        //userName 중복확인
        userRepository.findByUserName(userName).ifPresent(user -> {
            throw new AppException(ErrorCode.DUPLICATED_USER_NAME, ErrorCode.DUPLICATED_USER_NAME.getMessage());
        });
        //저장
        User savedUser = User.of(userName, encoder.encode(password));
        savedUser = userRepository.save(savedUser);
        //ResponseDTO
        UserJoinResponse userJoinResponse = UserJoinResponse.of(savedUser.getUserId(), savedUser.getUserName());
        return userJoinResponse;
    }
}

예외 처리

AppException

@AllArgsConstructor
@Getter
public class AppException extends RuntimeException {
    private ErrorCode errorCode;
    private String message;
}

ErrorCode

@AllArgsConstructor
@Getter
public enum ErrorCode {
    DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "UserName이 중복됩니다."),
    USERNAME_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 UserName입니다."),
    INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "패스워드가 다릅니다."),
    INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 토큰입니다."),
    INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "사용자가 권한이 없습니다."),
    POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 포스트가 없습니다."),
    DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "DB에러");

    private HttpStatus httpStatus;
    private String message;
}

ExceptionManager

@RestControllerAdvice
public class ExceptionManager {
    @ExceptionHandler(AppException.class)
    public ResponseEntity<Response> appExceptionHandler(AppException e) {
        ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode().name(), e.getMessage());
        Response response = Response.of("ERROR", errorResponse);
        return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(response);
    }
}

ErrorResponse

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class ErrorResponse {
    private String ErrorCode;
    private String message;

    public static ErrorResponse of(String errorCode, String message) {
        return ErrorResponse.builder()
                .ErrorCode(errorCode)
                .message(message)
                .build();
    }
}

패스워드 인코딩 처리

SecurityConfig

@Configuration
public class SecurityConfig {

    private String[] PERMIT_URL = {
            "/api/v1/join"
    };

    private String[] SWAGGER = {
            /* swagger v2 */
            "/v2/api-docs",
            "/swagger-resources",
            "/swagger-resources/**",
            "/configuration/ui",
            "/configuration/security",
            "/swagger-ui.html",
            "/webjars/**",
            /* swagger v3 */
            "/v3/api-docs/**",
            "/swagger-ui/**"
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers(SWAGGER).permitAll()
                .antMatchers(PERMIT_URL).permitAll()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .build();
    }
}

EncoderConfig

@Configuration
public class EncoderConfig {
    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }
}

🌉 회고

  • UserConroller의 테스를 하는 방법에 대해 복습을 해보면서 복기를 했다.
  • SpringSecurity 의존성을 추가하면 Test에서 api에 접근하는 것이 막힌다는 사실을 다시 인지하게 되었고, Spring Security Test 의존성을 추가하여 해결해야 한다는 사실을 복습했다.
  • 아직 Security에 대한 개념이 부족한 것 같아 이 주제에 대한 자세한 공부가 필요할 것 같다.
profile
주니어 개발자를 향해서..

0개의 댓글