4. 배달 플랫폼 - User

ys·2024년 3월 3일

배달플랫폼

목록 보기
5/8
  • 이제 기본 설정은 끝났으니, user 엔티티를 만들고 회원에 관련된 기능을 만들어보자
  • db 모듈의 org.delivery.db/user/UserEntity에 대해 먼저 만들어주자
  • db 모듈에서는 Entity와 repository에 대한 기능을 구현한다

BaseEntity

@Data
@SuperBuilder
@MappedSuperclass
@AllArgsConstructor
@NoArgsConstructor
public class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

UserEntity

@Entity
@Table(name = "user")
@Getter
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity extends BaseEntity {

    @Column(length = 50, nullable = false)
    private String name;

    @Column(length = 100, nullable = false)
    private String email;

    @Column(length = 100, nullable = false)
    private String password;

    @Column(length = 50, nullable = false)
    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @Column(length = 150,nullable = false)
    private String address;

    private LocalDateTime registeredAt;
    private LocalDateTime unregisteredAt;
    private LocalDateTime lastLoginAt;
    }

UserRepository

@Repository
public interface UserRepository extends JpaRepository<UserEntity,Long> {

    // select * from user where id =? and status =? order by id DESC limit 1;
    Optional<UserEntity> findFirstByIdAndStatusOrderByIdDesc(Long userId, UserStatus userStatus);

    // select * from user where email =? and password =? and status =? order bt id desc limit;
    Optional<UserEntity> findFirstByEmailAndPasswordAndStatusOrderByIdDesc(String email, String password, UserStatus userStatus);
}
  • JpaRepository<UserEntity,Long>을 상속받아 인터페이스로 구현한다
  • findFirstByIdAndStatusOrderByIdDesc라고 userid, userStatus를 이용해 옵셔널을 반환한다
  • findFirstByEmailAndPasswordAndStatusOrderByIdDesc라고 email, userid,userStatus를 이용해서 옵셔널을 반환한다

  • 이제 기능에 대한 구현을 생각해보겠다
  • Api 모듈에서 진행한다
  • service계층과 controller에 대한 구현부분이다
  • 그런데 우리는 이후에, token부분에 대한 로직도 들어가게 된다
  • 그렇게 된다면, user service 계층은, 단순히 user의 기능만 들어가는게 아니라 여러 다른 비지니스 로직이 들어가게 된다
  • 하나의 책임만을 주는 Solid의 SRP원칙에 위배된다... -> 결국 코드의 유지보수가 어려워지게 된다

어노테이션을 이용! -> Service계층의 SRP원칙 부여

  • 그래서 우리는 service의 역학을 생각해보게 되었다
  • 먼저 domian즉 정말 user에 대한 부분 : 회원등록, 로그인 기능등을 있겠다
  • 그리고 외부에 보내는 API 스펙에 따라 변환하는 부분
  • 다른 token등등의 로직과 합쳐지는 비지니스 로직 부분
  • 즉 service계층을, 역할에 따라 3가지의 책임으로 분리해보겠다

어노테이션을 이용해서, service로 컴포넌트 등록도 하고, 명시적으로 분리해줄 것이다

Business

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
public @interface Business {
    @AliasFor(annotation = Service.class)
    String value() default "";
}

Converter

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
public @interface Converter {
    @AliasFor(annotation = Service.class)
    String value() default "";
}
  • 다음과 같이 서비스 계층을 커스텀한 어노테이션을 이용해 나누어주어, 역할에 따른 책임을 분리해줄 것이다
  • controller와 service사이에 business라는 커스텀 어노테이션을 이용하는 것이다
  • 본질적으로 Business 커스텀 어노테이션도, Service어노테이션을 가지고 있다
  • 의존관계를 다음과 같이 설정한 것이다

UserService

  • setter사용을 지양하기 위해서
public void statusAndRegisteredAt(UserStatus status, LocalDateTime registeredAt){
        this.status=status;
        this.registeredAt=registeredAt;
    }
  • UserEntity에 statusAndRegisterdAt 메서드를 추가해주었다
/**
 * User 도메인 로직을 처리하는 서비스
 */
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public UserEntity register(UserEntity userEntity) {
        return Optional.ofNullable(userEntity)
                .map(it -> {
                    it.statusAndRegisteredAt(UserStatus.REGISTERED, LocalDateTime.now());
                    return userRepository.save(it);
                })
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "User Entity Null"));
    }

    public UserEntity login(String email, String password){
        UserEntity userWithThrow = getUserWithThrow(email, password);
        return userWithThrow;
    }
    public UserEntity getUserWithThrow(String email, String password){
        return userRepository.findFirstByEmailAndPasswordAndStatusOrderByIdDesc(
                email, password, UserStatus.REGISTERED
        ).orElseThrow(() -> new ApiException(UserErrorCode.USER_NOT_FOUND));

    }
    public UserEntity getUserWithThrow(Long userId){
        return userRepository.findFirstByIdAndStatusOrderByIdDesc(
                userId, UserStatus.REGISTERED
        ).orElseThrow(() -> new ApiException(UserErrorCode.USER_NOT_FOUND));

    }
}
  • 기능은 회원가입인 register기능이 있다
    • userEntity에 status를 REGISTERD로 바꾸고, 현재 시간을 등록하고
    • repository에 save해준다
    • 만약 받은 userEntity가 null이면 ApiException을 던져준다
  • 다음은 login 기능이다
    • email, password를 받고
    • 리파지토리에서 만들어두었던 findFirstByEmailAndPasswordAndStatusOrderByIdDesc 메서드를 이용해 UserEntity를 반환한다
    • 만약 없다면, UserException을 내려준다
    • 방금의 기능을 구현한 getUSerWithThrow를 login에서 구현해준다
  • getUSerWithThorw는 userid를 받고
    • userId를 통해 UserEntity를 찾아주는 것이다
    • 만약 없다면, User_Not_Found 예외를 내준다

항상 말했듯이, Api스펙으로 entity를 보내주면 위험하다

  • 그렇기에, 로그인할 때 필요한 데이터를 받는 UserLoginRequest와
  • 회원가입할 때 필요한 데이터를 받는 UserRegisterRequset와
  • User엔티티에 db가 만들어주는 pk를 포함해서 응답을 보내는 UserRespone즉
  • DTO 계층을 만들어준다

UserLoginRequest

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginRequest {

    @NotBlank
    private String email;

    @NotBlank
    private String password;
}
  • 로그인을 위한 email,password가 있다

UserRegisterdRequest

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserRegisterRequest {

    @NotBlank
    private String name;
    @NotBlank
    @Email
    private String email;
    @NotBlank
    private String address;
    @NotBlank
    private String password;
}
  • 등록에는 이름, 이메일,주소, 비밀번호를 받는다

UserResponse

@Data
@RequiredArgsConstructor
@AllArgsConstructor
@Builder
public class UserResponse {

    private Long id;

    private String name;

    private String email;


    private UserStatus status;

    private String address;

    private LocalDateTime registeredAt;
    private LocalDateTime unregisteredAt;
    private LocalDateTime lastLoginAt;


}
  • UserEntity에 db에서 만들어주는 pk도 포함해서 내려준다
  • 만약 db에서 다른 값을 필요로 한다면 스펙을 다르게 만들어서 내려주면 된다!!

UserConverter

@Converter
@RequiredArgsConstructor
public class UserConverter {
    public UserEntity toEntity(UserRegisterRequest request){
        return Optional.ofNullable(request)
                .map(it -> {
                    // to Entity
                    return UserEntity.builder()
                            .name(request.getName())
                            .email(request.getEmail())
                            .password(request.getPassword())
                            .address(request.getAddress())
                            .build();

                })
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "UserRegisterRequest Null"));
    }

    public UserResponse toResponse(UserEntity userEntity) {

        return Optional.ofNullable(userEntity)
                .map(it->{
                    // to response
                    return UserResponse.builder()
                            .id(userEntity.getId())
                            .name(userEntity.getName())
                            .status(userEntity.getStatus())
                            .email(userEntity.getEmail())
                            .address(userEntity.getAddress())
                            .registeredAt(userEntity.getRegisteredAt())
                            .unregisteredAt(userEntity.getUnregisteredAt())
                            .lastLoginAt(userEntity.getLastLoginAt())
                            .build();
                })
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "UserEntity Null"));
    }
}
  • 등록 데이터 UserRegisterRequest를 받아서 builder 패턴을 이용해 UserEntity를 등록하는 toEntity 매서드를 만든다
  • 이제 userEntity를 UserResponse로 보내는 메서드인 toResponse도 만들어준다
  • 둘다 optional이므로 orElseThrow를 통해 맞는 오류를 던져준다

UserBusiness

@Business
@RequiredArgsConstructor
public class UserBusiness {

    private final UserService userService;
    private final UserConverter userConverter;

    /**
     * 사용자에 대한 가입처리 로직
     * 1. request -> entity
     * 2. entity -> save
     * 3. save Entity -> response
     * 4. response return
     */
    public UserResponse register(UserRegisterRequest request) {

        /*UserEntity entity = userConverter.toEntity(request);
        UserEntity newEntity = userService.register(entity);
        UserResponse response = userConverter.toResponse(newEntity);
        return response;*/

        return Optional.ofNullable(request)
                .map(it->userConverter.toEntity(it))
                .map(it->userService.register(it))
                .map(it->userConverter.toResponse(it))
                .orElseThrow(() -> new ApiException(ErrorCode.NULL_POINT, "request null"));
                }

    }
  • 회원가입 로직은, register로 UserRegisterRequest를 받으면
  • converter을 이용해 -> UserEntity로 바꾼 후, db에 저장
  • 저장 후, 응답을 보낼때, UserEntity -> UserResponse로 바꿔서
  • UserRresponse을 전달한다

UserOpenApiController

  • 우리는 로그인 기능이 없는 api를 open-api로 하기로 하였다
  • 지금은 로그인을 위한 회원가입과, 로그인 기능이므로
  • 권한이 없는 open-api에 만들어준다
@RestController
@RequiredArgsConstructor
@RequestMapping("/open-api/user")
public class UserOpenApiController {

    private final UserBusiness userBusiness;

    // 사용자 가입 처리
    @PostMapping("/register")
    public Api<UserResponse> register(@Valid @RequestBody Api<UserRegisterRequest> request){
        UserResponse response = userBusiness.register(request.getBody());
        return Api.OK(response);
    }

    // 로그인
    @PostMapping("/login")
    public Api<TokenResponse> login(@Valid @RequestBody Api<UserLoginRequest> request){
       ... 
       // Todo : token검증 기능 business계층에 추가하기
        return Api.OK(response);
    }
}
  • Api에다가 UserRegisterRequest를 담아서 받는다
  • request에서 getBody를 통해 body의 UserRegisterRequest를 꺼낸후,
  • userBusiness의 register메서드를 통해서 (변환,저장,변환)을 한 후,
  • UserResponse형태로 반환한다
  • 이 부분을 Api의 body부분에 넣고, result에 Ok를 넣어서 반환해준다
  • 로그인 기능은 business에 token검증 부분을 넣은 후에 추가 설명하겠다

추가 기능 .. token

  • 우리는 비지니스 로직에, 로그인한 user는 token을 이용해서, 권한을 가져 로그인 이후의 페이지에 접근할 수 있다
  • 우리는 인터페이스에 다음 기능등을 Todo로 구현해 두었다
  • 다음 기능등을 구현할 것이다
  • token 생성, 재인증, 검증 기능등을 구현한다!
                                         
profile
개발 공부,정리

0개의 댓글