본문 바로가기

프로젝트/멋사 개인 프로젝트 (mutsa-SNS)

[03] mutsa-SNS 1일차 - (2) 회원 가입 기능

회원 가입 기능을 구현하였다.

클라이언트에게 userName와 password를 받아 회원가입을 진행한다.

1. Domain

UserEntity

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Table(name = "user")
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String password;
    private Timestamp registeredAt;
    private String role;
    private Timestamp updatedAt;
    @Column(unique = true)
    private String userName;

}

user의 정보를 db에 저장하기 위한 Entity를 생성하였다.

id는 자동으로 증가하게 설정하였다.

userName은 중복을 불허하기 위해 unique 설정을 하였다.

 

UserDto (domain - dto directory)

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class UserDto {

    private int id;
    private String userName;
    private String password;
    private String role;


}

service에서 controller에서 user정보를 전달하기 위한 dto이다.

 

UserJoinRequest(domain - dto directory)

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class UserJoinRequest {

    private String userName;
    private String password;

    public User toEntity(String password, String role, Timestamp time) {
        return User.builder()
                .userName(this.userName)
                .password(password)
                .role(role)
                .registeredAt(time)
                .updatedAt(time)
                .build();
    }
}

회원 가입시 클라이언트에게 요청할 내용을 담고있는 dto이다.

회원 가입시 userName과 password를 입력 받는다.

service에서 db에서 회원가입한 유저의 정보를 저장하기 위한 Entity로 변환하는 class를 구현하였다.

 

Response (domain - dto directory)

@AllArgsConstructor
@Getter
public class Response<T> {

    private String resultCode;
    private T result;

    public static <T> Response<T> error(T result) {
        return new Response("ERROR", result);
    }

    public static <T> Response<T> success(T result) {
        return new Response("SUCCESS", result);
    }
}

모든 response는 해당 class로 감싸서 return된다.

성공과 실패를 구분하는 resultCode와 내용을 담는 result로 구성되어 있다.

 

UserJoinResponse (domain - dto directory)

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class UserJoinResponse {

    private int id;
    private String userName;

}

controller에서 회원 가입시 view에 보여주게 되는 response이다.

회원 가입 성공 시 respose class의 result에 해당 내용이 담기게 된다.

 

ErrorResponse (domain - dto directory)

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class ErrorResponse {

    private String errorCode;
    private String message;

}

exception발생 시  respose class의 result에 해당 내용이 담기게 된다.

 

2. Exception

ErrorCode

@AllArgsConstructor
@Getter
public enum ErrorCode {
    DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "UserName이 중복됩니다."),
    NOT_FOUNDED_USER_NAME(HttpStatus.NOT_FOUND, "UserName is not founded"),

    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 status;
    private String message;


}

enum으로 구성되어 있다.

해당 name에 해당되는 HttpStatus와 errorMessage를 담고 있다.

 

AppException

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

    @Override
    public String toString() {
        if(message.equals("")) {
            return errorCode.getMessage();
        }

        return String.format("%s, %s", errorCode.getMessage(), message);
    }


}

errorCode Enum과 message를 설정 가능하게 하였다.

만약 service에서 따로 message를 추가한다면, errorCode의 message에 추가하여 message가 출력될 수 있도록 toString을 override하였다.

 

ExceptionManager

@RestControllerAdvice //모든 컨트롤러의 예외를 처리
public class ExceptionManager {

    @ExceptionHandler(AppException.class)
    public ResponseEntity<?> AppExceptionHandler(AppException e) {

        return ResponseEntity.status(e.getErrorCode().getStatus())
                .body(Response.error(new ErrorResponse(e.getErrorCode().name(), e.toString())));
    }

}

발생한 모든 exception이 해당 class에서 처리된다.

view에 response.error 를 출력하며, Enum ErrorCode에서 설정한 이름과 message를 출력한다.

httpStatus또한 Enum ErrorCode에서 설정한 값으로 설정된다.

 

<Reference>

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

 

401 Unauthorized - HTTP | MDN

The HyperText Transfer Protocol (HTTP) 401 Unauthorized response status code indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource.

developer.mozilla.org

 

3. Repository

UserRepository

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

Jpa를 사용하였다. db에 저장되어있는 user에서 입력한 userName에 해당되는 내용을 불러오기 위해 설정하였다.

없는 경우의 error를 막기 위해 Optional을 적용하였다.

 

4. Service

UserService

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    public UserDto join(UserJoinRequest req) {

        //중복 userName시 error
        userRepository.findByUserName(req.getUserName())
                .ifPresent(user -> {
                    //throw new AppException(ErrorCode.DUPLICATED_USER_NAME, String.format("userName : %s는 이미 있습니다.", req.getUserName()));
                    throw new AppException(ErrorCode.DUPLICATED_USER_NAME, "");
                });

        //UserRole index
        String role = "USER";

        //db에 유저가 아무도 없을 때, 새로 생성되는 계정을 admin으로
        if (userRepository.count() == 0) {
            role = "ADMIN";
        }

        Timestamp registeredAt = new Timestamp(System.currentTimeMillis());

        User savedUser = userRepository.save(req.toEntity(encoder.encode(req.getPassword()), role, registeredAt));

        return UserDto.builder()
                .id(savedUser.getId())
                .userName(savedUser.getUserName())
                .role(savedUser.getRole())
                .build();
    }
 }

중복 userName이 있을 때 exception을 통해 처리한다.

db에 유저가 없을 때, 처음 등록한 계정을 ADMIN role을 부여한다.

현재 시간은 System.currentTimeMillis()를 이용하였다.

password를 암호화 하기 위해 BCryptPasswordEncoder를 사용하였다. 이는 Configuration에 따로 class로 설정해 주어야 한다. (EncrypterConfig)

클라이언트가 입력한 userName, password를 토대로 Entity로 변환하여 db에 저장한 후, userDto형태로 controller에 return한다. 이 때 보안상 password는 전송하지 않았다.

 

5. Controller

UserController

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

    private final UserService userService;

    @PostMapping("/join")
    public Response<UserJoinResponse> join(@RequestBody UserJoinRequest req) {
        UserDto userDto = userService.join(req);
        return Response.success(new UserJoinResponse(userDto.getId(), userDto.getUserName()));
    }

}

service에 클라이언트가 입력한 userName과 password를 준다.

service가 return한 userDto에서 id와 userName을 response.success에 result에 담아 view에 보여준다.

 

6. Configuration

EncrypterConfig

@Configuration
public class EncrypterConfig {
    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder(); //password를 incoding해줄때 쓰기 위함
    }
}

service에서 password 암호화에 사용한 BCryptPasswordEncoder를 설정해 주었다.

BCryptPasswordEncoder를 사용하기 위해서는 build.graddle에 spring security에 대한 dependency를 추가해준 후, configuration class에서 Bean으로 등록하여 spring container 에 등록하면 된다.

 

BCryptPasswordEncoder 란?

Spring Seurity에서 제공하는 ****비밀번호를 암호화하는 데 사용할 수 있는 method를 가진 class이다.

.encode() : 패스워드를 암호화해준다.

.matches() : 인코딩 되지 않은 암호와 저장소에 있는 인코딩된 암호가 일치하는지 확인. boolean형태로 return 해준다.

 

<Reference>

https://bestinu.tistory.com/60

 

[Spring Security] BCryptPasswordEncoder란?

👀BCryptPasswordEncoder란? BCryptPasswordEncoder는 Spring Security 프레임워크에서 제공하는 클래스로 비밀번호를 암호화(해시)하는 데에 사용한다. 해시 함수에는 MD5나 SHA 등의 종류가 있지만 BCrypt는 단순

bestinu.tistory.com

 

<AWS 배포 URL>

http://ec2-3-35-225-29.ap-northeast-2.compute.amazonaws.com:8080/swagger-ui/

 

<전체 reference>

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

 

401 Unauthorized - HTTP | MDN

The HyperText Transfer Protocol (HTTP) 401 Unauthorized response status code indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource.

developer.mozilla.org

https://bestinu.tistory.com/60

 

[Spring Security] BCryptPasswordEncoder란?

👀BCryptPasswordEncoder란? BCryptPasswordEncoder는 Spring Security 프레임워크에서 제공하는 클래스로 비밀번호를 암호화(해시)하는 데에 사용한다. 해시 함수에는 MD5나 SHA 등의 종류가 있지만 BCrypt는 단순

bestinu.tistory.com