Skip to content

Conversation

@daydream-03
Copy link
Collaborator

Description

사용자 회원가입 및 로그인 기능을 구현했습니다.

Changes

  • 기반 구조: JPA Entity, DTO, Repository 계층 구현
  • 서비스 로직: Spring Security와 BCrypt를 활용한 인증 시스템 구현
  • API 엔드포인트: REST API를 통한 회원가입/로그인 기능 제공

Technical Details

  • User Entity: JPA 어노테이션을 활용한 데이터베이스 매핑
  • DTO Classes: UserCreateRequest, UserLoginRequest DTO 구현
  • Repository: findByUsername, findByEmail 메서드 추가
  • SecurityConfig: Spring Security 설정 및 API 접근 허용
  • UserService: 회원가입 검증 및 로그인 인증 로직 구현
  • UserController: POST /api/v1/users/register, /login 엔드포인트 구현

Testing

  • Postman을 통한 API 테스트 완료
  • 회원가입 및 로그인 기능 정상 동작 확인
  • MySQL 데이터베이스에 사용자 정보 정상 저장 확인

Checklist

  • 회원가입 API 구현
  • 로그인 API 구현
  • Spring Security 설정
  • BCrypt 패스워드 암호화
  • 데이터베이스 연동
  • 에러 처리 구현

🔗 Related Issues

Closes #1

@daydream-03 daydream-03 requested a review from f-lab-moony August 3, 2025 07:27
@daydream-03 daydream-03 self-assigned this Aug 3, 2025
@daydream-03 daydream-03 changed the base branch from main to dev August 3, 2025 07:29
@daydream-03 daydream-03 force-pushed the feature/1-user-login-register branch from 5aba919 to aa833ba Compare August 3, 2025 07:40
Copy link
Collaborator

@f-lab-moony f-lab-moony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다 ~ 피드백 확인 부탁드려요!

import org.springframework.security.web.SecurityFilterChain;


@Configuration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 어플리케이션 수행 해 보셨나요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 직접 포스트맨으로 회원가입, 로그인 동작하는 것 확인하고 DB에도 사용자 레코드 확인했습니다

http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/v1/users/register").permitAll()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다 permitAll인데 나눌 필요가 있을까요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단은 나누는 것은 고려하지 않고 진행하겠습니다. (사실 어드민 등급을 따로 만들어서 어드민 사용자만 호출할 수 있는 api들도 추가하려 했는데, 이건 조금 먼 나중의 일이 될 것 같아서요)

return "users";
}

@PostMapping("/register")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성요청도 Post로 제어가 가능해서 register는 굳이 안붙여도 될 것 같아요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

}

@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody UserCreateRequest userCreateRequest) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 저번시간에 말씀드렸던 것 같은데, ok 이외의 응답은 여기서 처리 할 이유가 없어서 ResponseEntity는 안써주셔도 될 것 같아요 ~ 그리고 와일드카드(?)는 사용을 지양해주세요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

public ResponseEntity<?> registerUser(@RequestBody UserCreateRequest userCreateRequest) {
try {
Long userId = userService.save(userCreateRequest);
return ResponseEntity.ok().body("User registered successfully with ID: " + userId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

body는 왜 이렇게 응답하신걸까요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 응답 dto 작업이 안되어있어서 일단 성공/실패 여부를 확인하기 위해 적어두었습니다
멘토님 질문의 의도가 혹시 응답 dto가 있어야하는데 없어서 물어보신 걸까요?

validateUserRegistration(userCreateRequest);

// 새 사용자 생성
User user = new User();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setter대신 생성자를 이용해서 만드는 건 어떨까요? 그리고 lombok을 이용하면 builder 패턴을 쉽게 사용하실 수 있어요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lombok 학습해서 적용해보겠습니다

this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}

public Long save(UserCreateRequest userCreateRequest) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db작업은 @Transactional을 붙여주세요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

return savedUser.getId();
}

public User login(UserLoginRequest userLoginRequest) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 관련한건 별도의 서비스나 Dto를 확장 한 애들이 필요하지 않나요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분명 개념적으로는 단일책임원칙을 학습했지만 실제로 적용하여 분리하는 것이 미숙한 것 같습니다. 😭
UserService 안에서 User의 모든 로직을 처리한다- 라는 인식이 박혀있어서, 코드 작성 당시에 분리할 생각조차 못했는데 수정하겠습니다

}

private void validateUserRegistration(UserCreateRequest userCreateRequest) {
if (userRepository.findByUsername(userCreateRequest.getUsername()).isPresent()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 검증을 해도 괜찮긴한데, 유니크 제약조건은 DB에 걸어놓기만 해도 커밋 시에 예외가 발생하니 굳이 안잡아줘도 된다고 생각해요. 검증을 위해 조회 쿼리가 매번 나가야해서요

throw new RuntimeException("Username already exists: " + userCreateRequest.getUsername());
}

if (userRepository.findByEmail(userCreateRequest.getEmail()).isPresent()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 유효성검사할때 guava 라이브러리에 있는 Preconditions 사용하면 좀 사용하기 편하실거같아요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 학습해서 적용해보겠습니다

Copy link
Collaborator

@f-lab-moony f-lab-moony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다 ~
이번 피처는 여기서 머지 할게요 ~

import java.io.IOException;

@Component
@RequiredArgsConstructor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RequiredArgsConstructor 의 단점은 어떤게 있을까요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 명시적 생성자가 없어서 어떤 필드가 필수인지 한눈에 파악하기 어렵습니다.
  2. 선택적으로 설정될 수 있는 필드에 대해서는 생성자를 자동으로 생성해주지 않기 때문에 직접 추가해야합니다.
  3. Autowired는 스프링이 제공 주체이지만, RequiredArgsConstructor는 Lombok에서 제공하기 때문에 Lombok 의존성 추가가 강제됩니다.

크게 3가지 정도 학습해서 답변드립니다


@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jwt를 쓰기로 한 이유가 있을까요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

솔직히 말씀드리면 토큰이 필요하다는 생각이 들었을 때 가장 먼저 떠오른게 JWT여서-이긴 했는데요,
토큰 안에 필요한 정보들을 선별해 담을 수 있고, 서버 어딘가에 인증정보를 저장해야하는 세션 기반 방식과는 다르게 stateless하다는 장점들이 있다고 알고 있습니다.


@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스타일가이드가 없는 것 같은데요, 스타일가이드 적용 예제 보고 한번 따라해보시면 좋을 것 같아요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다


private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런건 상수로 정의해보면 좋을 것 같아요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

try {
String jwt = getJwtFromRequest(request);

if (jwt != null && jwtTokenProvider.validateToken(jwt)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null체크같은 경우는 조건이 맞지 않는 경우 얼리리턴을 해주는게 가독성에 도움이 될 것 같아요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

} catch (Exception e) {
return ResponseEntity.badRequest().body("Registration failed: " + e.getMessage());
}
@PostMapping("")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 문자열은 굳이 필요 없어보여요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다

return ResponseEntity.badRequest().body("Registration failed: " + e.getMessage());
}
@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 좀 생각해 볼 필요가 있는데요, 클라이언트 사이드에서 성공 응답을 200번대 로 잡는게 아니라 200 을 잡는 경우가 많아서 성공 응답을 세분화하진 않는 편이에요. 물론 잘못된건 아니고 제가 겪지 못한 부분에서 나누는게 의미가 있을 수도 있겠지만, 제가 경험한 범위에서는 그랬었고, 당장 생각해봤을때 성공 응답을 세분화하는게 의미가 있나 싶긴 하네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성공 응답 세분화까지는 안하는 방향으로 수정하겠습니다

} catch (Exception e) {
return ResponseEntity.badRequest().body("Login failed: " + e.getMessage());
}
public ResponseEntity<UserLoginResponse> loginUser(@Valid @RequestBody UserLoginRequest userLoginRequest) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거는 몇번 말씀드렸는데, 그냥 응답에 성공하면 기본적으로 200 응답이 내려가기때문에 굳이 응답을 감싸진 않아도 될 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드를 쓰고 지우고 반복하는 과정에서 또 되살아난 것 같습니다. 주의하겠습니다!

public String getEmail() {
return email;
}
@NotBlank(message = "Email is required")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유효성검사를 여기서 하는건 좀 생각해볼 필요가 있는데요, 설정이 쉽고 요청단에서 어떤 값을 가지는지 의미를 명확히 할 수 있다는 장점도 있지만, 코드 침투가 일어나서 가독성이 떨어지는 것과, 어차피 서비스단에서 유효성검사를 하기 때문에 중복으로 검사를 하게 되고 관리 포인트가 늘어나는 단점이 있어요.

컨트롤러 - 서비스 - 도메인 중 유효성검사는 어디서 해야하나 라는걸로 의견이 갈릴때가 많은데, 굳이 따지자면 저는 도메인에 가까운 순으로 하는게 맞다고 생각하고, 제일 좋은건 각 레이어에서 각각 하는게 제일 좋긴 하다고 생각합니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨트롤러, 서비스, 도메인 3개의 레이어에서 각각 유효성검사를 수행하되, dto에서까지는 수행하지 않는 것으로 이해했습니다.
수정하겠습니다.

@f-lab-moony
Copy link
Collaborator

제가 머지 권한이 없네요? 직접 머지 해주시고 저 권한좀 주세요 ~

@daydream-03 daydream-03 merged commit 7791b28 into dev Aug 11, 2025
1 check passed
daydream-03 added a commit that referenced this pull request Oct 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

사용자 로그인, 회원가입 구현

3 participants