Skip to content

[feat] 회원가입, 로그인, 유저정보 수정 기능 구현#12

Open
junjinyun wants to merge 16 commits intomainfrom
feat/#9
Open

[feat] 회원가입, 로그인, 유저정보 수정 기능 구현#12
junjinyun wants to merge 16 commits intomainfrom
feat/#9

Conversation

@junjinyun
Copy link
Copy Markdown
Contributor

[feat] 회원가입, 로그인, 유저정보 수정 기능 구현

😺 Issue

✅ 작업 리스트

  • 회원가입 기능 구현
  • 로그인 기능 구현
  • 회원정보 수정 구현
  • 서비스 각 기능 및 예외처리에 대한 테스트 코드 작성
  • 실제 DB에 회원가입, 로그인, 정보수정, 회원삭제 하는 테스트 코드 작성

⚙️ 작업 내용

  • 회원 가입 시 이름, 이메일, 비밀번호 입력받으며, 자동으로 UID, createdAt(계정 생성 일자) 를 부여하며, 비밀번호는 해싱하여 저장
  • 로그인 시 입력받은 이메일, 비밀번호 중 비밀번호는 해싱하며, 이메일이 있는지 1차로 찾고 2차로 비밀번호를 비교해서 처리
  • 회원정보 수정 시 {계정이름, map(속성명, 변경 후 값)} 형식으로 요청받아서 처리하며 ,이메일, 계정생성일 같은 속성은 수정 불가능
  • 유효성검사, 중복 체크, 사용자 없음 등의 기본적인 예외처리 완료 및 해당 오류 발생 시 프론트에 commonResponce로 (true,해당 오류에 대한 설명) 형식으로 커스텀 오류 문구 반환

📷 테스트 / 구현 내용

image image image

@dohy-eon dohy-eon self-requested a review July 14, 2025 16:11
@dohy-eon dohy-eon added 👾 feat 새 기능 / New features 🐯 준진 labels Jul 14, 2025
@dohy-eon dohy-eon linked an issue Jul 14, 2025 that may be closed by this pull request
@dohy-eon dohy-eon requested review from Vloeiolzlr and ysw789 July 14, 2025 16:31
@kjunh972
Copy link
Copy Markdown
Contributor

@dohy-eon 도현님 이번에 피드백하면서 노션에 정리되었던 폴더 구조를 확인해 보았는데 계층형 구조로 되어있는것 같은데
기능/도메인별(예: user, capsule 등)로 하위 패키지를 만들어서 각 도메인 내에 controller, service, dto, entity, repository를 그룹화하는 구조가 낫지 않을까요? 너무 파일들이 많아서 나중에 좀 난잡해실것 같아서 살짝 걱정이 됩니다.

도메인 단위로 관련 소스가 한눈에 모여 있어서 신규 기능 개발 작업할때나 가독성 측면에서 더 효율적일것 같습니다. 근데 이번에 스프링 처음한 팀원들이 많아서 이렇게 설계하신거라면.. 괜찮을것 같습니다. 그냥 걱정이되서 한번 여쭤봤습니당..

src/main/java/com/dasom/MermoReal/domain/
    ├── user
    │    ├── controller
    │    ├── service
    │    ├── dto
    │    ├── entity
    │    └── repository
    ├── capsule
    │    ├── controller
    │    ├── service
    │    ├── dto
    │    ├── entity
    │    └── repository

@kjunh972
Copy link
Copy Markdown
Contributor

kjunh972 commented Jul 14, 2025

@dohy-eon
아 그리고 도현님 특히나 계층형 구조로 갈꺼면 dto를 책임 분리해서 관리하는게 낫지 않나요?

└── domain
        ├── controller
        │   └── dto   
        ├── service
        │   └── dto  

기존 구조처럼 DTO를 한 군데 몰아서 관리하면 Controller용 DTO와 Service/Domain용 DTO가 섞일 가능성이 높고 이렇게 되면 도메인이 커질수록 관리가 어려워지고 실수도 더 많아질 것 같습니다.
DTO가 몰려 있으면 나중에 책임이 불분명해지고 API와 서비스 로직이 섞여서 유지보수, 협업, 확장성 측면에서 오히려 더 힘들것 같습니다.

클린 아키텍처 관점에서도 계층별, 목적별로 DTO를 분리하면 각 역할에 집중할 수 있고 추가, 변경, 확장에도 훨씬 유리하다고 생각합니다.

제가 생각한건 이런구조가 베스트라 생각하긴 했습니다.

src/main/java/com/dasom/MermoReal/domain/
    ├── user
    │    ├── controller
    │    │    └── dto   
    │    ├── service
    │    │    └── dto   
    │    ├── entity
    │    └── repository
    │    │    └── dto   
    ├── capsule
    │    ├── controller
    │    │    └── dto   
    │    ├── service
    │    │    └── dto   
    │    ├── entity
    │    └── repository

진짜 완전히 같으면 공통 DTO로 합쳐도 되지만 각 레이어가 필요하면 각자 dto 만들어서 쓰는 게 맞다라고 생각이 들긴합니다.
이렇게 하면 도메인별로 관련 소스가 한눈에 모여 있어서 작업할 때나 코드 리뷰, 신규 기능 추가/변경 시 더 효율적이고 책임 분리, 실수 방지, 협업에도 도움이 될 것 같습니다.

스프링을 이번에 처음 하는 팀원이 많아서 현재 구조도 충분히 괜찮다고 생각합니다. 근데 나중에를 고려해서 한번 참고해주시면 좋을 것 같습니다.

@kjunh972
Copy link
Copy Markdown
Contributor

@junjinyun @dohy-eon 코드상에 api 문서화 안되어있는데 팀내에서 swagger나 REST Docs중 어떤거 도입하기로 결정이 됐나여?

Copy link
Copy Markdown
Contributor

@Vloeiolzlr Vloeiolzlr left a comment

Choose a reason for hiding this comment

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

ResponseEntity 방식보다 ApiResponse를 선택한 이유가 무엇인지,
테스트는 soutv 뿐만 아니라 JUnit5 라이브러리를 이용해주세요.
서비스 클래스의 코드가 전체적으로 복잡한데, 직관적이고 간단하게 작업주세요.

@dohy-eon
Copy link
Copy Markdown
Member

@dohy-eon 아 그리고 도현님 특히나 계층형 구조로 갈꺼면 dto를 책임 분리해서 관리하는게 낫지 않나요?

└── domain
        ├── controller
        │   └── dto   
        ├── service
        │   └── dto  

기존 구조처럼 DTO를 한 군데 몰아서 관리하면 Controller용 DTO와 Service/Domain용 DTO가 섞일 가능성이 높고 이렇게 되면 도메인이 커질수록 관리가 어려워지고 실수도 더 많아질 것 같습니다. DTO가 몰려 있으면 나중에 책임이 불분명해지고 API와 서비스 로직이 섞여서 유지보수, 협업, 확장성 측면에서 오히려 더 힘들것 같습니다.

클린 아키텍처 관점에서도 계층별, 목적별로 DTO를 분리하면 각 역할에 집중할 수 있고 추가, 변경, 확장에도 훨씬 유리하다고 생각합니다.

제가 생각한건 이런구조가 베스트라 생각하긴 했습니다.

src/main/java/com/dasom/MermoReal/domain/
    ├── user
    │    ├── controller
    │    │    └── dto   
    │    ├── service
    │    │    └── dto   
    │    ├── entity
    │    └── repository
    │    │    └── dto   
    ├── capsule
    │    ├── controller
    │    │    └── dto   
    │    ├── service
    │    │    └── dto   
    │    ├── entity
    │    └── repository

진짜 완전히 같으면 공통 DTO로 합쳐도 되지만 각 레이어가 필요하면 각자 dto 만들어서 쓰는 게 맞다라고 생각이 들긴합니다. 이렇게 하면 도메인별로 관련 소스가 한눈에 모여 있어서 작업할 때나 코드 리뷰, 신규 기능 추가/변경 시 더 효율적이고 책임 분리, 실수 방지, 협업에도 도움이 될 것 같습니다.

스프링을 이번에 처음 하는 팀원이 많아서 현재 구조도 충분히 괜찮다고 생각합니다. 근데 나중에를 고려해서 한번 참고해주시면 좋을 것 같습니다.

확인했습니다! 저도 개발 초기에 controller 와 service dto는 나누는게 맞지 않을까라는 고민을 많이 했는데요. 현재 dto와 entity 개념도 확실히 잡혀있는 상태가 아니라고 생각되어 통합 dto 폴더로 관리를 진행한 뒤 추후 (중간 발표 이후) 3학년 한분과 함께 리팩토링 및 고도화 작업을 진행하려고 했습니다. 코멘트 감사합니다!

@dohy-eon
Copy link
Copy Markdown
Member

@dohy-eon 도현님 이번에 피드백하면서 노션에 정리되었던 폴더 구조를 확인해 보았는데 계층형 구조로 되어있는것 같은데 기능/도메인별(예: user, capsule 등)로 하위 패키지를 만들어서 각 도메인 내에 controller, service, dto, entity, repository를 그룹화하는 구조가 낫지 않을까요? 너무 파일들이 많아서 나중에 좀 난잡해실것 같아서 살짝 걱정이 됩니다.

도메인 단위로 관련 소스가 한눈에 모여 있어서 신규 기능 개발 작업할때나 가독성 측면에서 더 효율적일것 같습니다. 근데 이번에 스프링 처음한 팀원들이 많아서 이렇게 설계하신거라면.. 괜찮을것 같습니다. 그냥 걱정이되서 한번 여쭤봤습니당..

src/main/java/com/dasom/MermoReal/domain/
    ├── user
    │    ├── controller
    │    ├── service
    │    ├── dto
    │    ├── entity
    │    └── repository
    ├── capsule
    │    ├── controller
    │    ├── service
    │    ├── dto
    │    ├── entity
    │    └── repository

ㅜㅜ 여담이지만 도메인 별 구조로 짜여있습니다,,,
image
이렇게 되어있고 준진씨가 처음 작업하시느라 헷갈리셔서 폴더 구조화가 어쩌다보니 계층형 구조로 바뀐 것 같습니다. 수정하도록 하겠습니다!

@dohy-eon
Copy link
Copy Markdown
Member

@kjunh972 위에 작성드린대로 패키지는 도메인 기준이고, 그 안엔 레이어 별로 분리하도록 작업 할 예정입니다. 확인 후 코멘트 남겨주시면 감사드리겠습니다.

@dohy-eon
Copy link
Copy Markdown
Member

@junjinyun @dohy-eon 코드상에 api 문서화 안되어있는데 팀내에서 swagger나 REST Docs중 어떤거 도입하기로 결정이 됐나여?

Swagger 관련해서 개인별로 스터디 진행하는 중입니다. REST Docs보단 훨씬 러닝커브가 낮다고 생각되어(테스트 코드도 안짜도 되구요...) 스웨거로 진행 할 예정입니다.

@dohy-eon dohy-eon requested a review from kjunh972 July 15, 2025 00:18
Copy link
Copy Markdown
Member

@dohy-eon dohy-eon left a comment

Choose a reason for hiding this comment

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

리뷰는 일단 간단하게 남겼습니다! 오늘 오후 10시 회의 전까지 한번 쭉 '읽어보시고' 너무 어려운 부분들은 검색해서 오시면 좋을 것 같습니다. 위에 동민이형이 리뷰 다신 것도 한번 생각해오시고,

코멘트 세세하게 단 거 제외하고 전체적으로는 일단

  1. 도메인 폴더 아래에 User 폴더 만들어서 그 아래에 Controller, dto, entity, repo, service)넣기
  2. 컨트롤러마다 try-catch 반복하지 말고 @RestControllerAdvice 이 어노테이션 사용해서 전역으로 관리하기 (이건 아래 참고 링크 두개 남겨놓을게요, 시간 나시면 쭉 읽어보고 이해해오시면 될 것 같습니다.)

정도입니다. 수고하셨고 10시 회의 때 뵙겠습니다!
RestControllerAdvice로 전역에서 발생된 예외 처리하기.
Enum 타입으로 에러 메시지 및 응답 관리하기

String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader;
String username = JwtUtil.extractUsername(token);

User updatedUser = userService.updateUserInfoByMap(username, updates);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

1-1. UserController에서 username 추출해서 updateUserInfoByMap()으로 보내는데 서비스 레이어에서 왜 String email로 받고 있고 현재 이 코드가 어떤 플로우로 동작하는지가 의문입니다... 일단 실수인 경우에 해결안으로는 jwt 발급할 때 subject에 username 말고 email 넣으면 해결될 문제라고 생각되는데 어떻게 테스트 코드가 돌아가는지를 모르겠네요😥

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. 현재 서비스 계층에서 UserRegisterRequest → User 변환을 직접 하고 있는데요, 변환 책임은 DTO에서 진행하는게 유지보수 측면에서 차이가 나기 때문에 DTO에
public User toEntity(String encodedPassword) {
        return new User(username, email, encodedPassword);
    } 

이런 식으로 메서드를 추가하는게 좋아보입니다.

}

@Transactional
public User updateUserInfoByMap(String email, Map<String, Object> updates) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

1-2. 여긴 왜 이메일일까요..?

@kjunh972
Copy link
Copy Markdown
Contributor

@kjunh972 위에 작성드린대로 패키지는 도메인 기준이고, 그 안엔 레이어 별로 분리하도록 작업 할 예정입니다. 확인 후 코멘트 남겨주시면 감사드리겠습니다.

아하 넵 이해했습니다. 노션은 users를 제가 못봤네여.. 😢
답변 감사합니다!

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재 작성하신 dto를 확인해보았는데

현재 LoginRequest, LoginResponse를 별도 파일로 작업해주셨는데 해당 DTO는 사실상 하나의 Login에서만 사용되는 요청/응답 구조입니다. 다른 dto 파일도 다 별도 파일로 만들어주신것 같습니다.

내용이 많지도 않고 이런 경우에는 이너 클래스 로 하나의 파일에 묶는 것이 더 적절한것 같습니다.

구조적으로 나중에 API가 많아지면 domain/dto 패키지에 파일이 너무 많아지는 문제가 생기므로 요청/응답 DTO는 되도록 관련 기능 기준으로 이너클래스로 작업을 해주시는게 나을것 같습니다.

public class LoginDto {

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Request {
        private String email;
        private String password;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Response {
        private String jwt;
    }
}

LoginResponse의 경우 필드명은 jwtaccessToken을 응답하는 건가요? 언제 뭘 응답하는지 네이밍이 애매한것 같습니다. accessToken이랑 refreshToken 기간산정도 고려도 되어있는 건가요?

Optional<User> findByUsername(String username);
boolean existsByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmailAndPassword(String email, String password);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UserRepository에 정의된 existsByEmailAndPassword암호화된 비밀번호를 DB에서 직접 조회하는 방식이기 때문에 보안상 잘못된 설계입니다.
그리고 existsByEmailAndPassword 사용되지 않고 있는것 같은데 사용하지 않는 코드는 제거해주세요.

public User login(String email, String rawPassword) {
return repository.findByEmail(email)
.filter(user -> passwordEncoder.matches(rawPassword, user.getPassword()))
.orElse(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재는 로그인할때 유저정보가 없으면 null을 반환하고 있는데 보통 인증 로직에서는 명시적 예외를 던지는 것이 일반적입니다.

@Transactional(readOnly = true)
public User login(String email, String rawPassword) {
    User user = repository.findByEmail(email)
            .orElseThrow(() -> new IllegalArgumentException("이메일이 존재하지 않습니다."));

    if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
        throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
    }

    return user;
}

서비스 레이어에서는 실패를 예외로 처리하고 컨트롤러에서 예외를 받아 HTTP 응답 코드로 매핑하는 것이 맞는 설계 같습니다.

import java.util.Date;

public class JwtUtil {
private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

현재 JwtUtil에서는 아래와 같은 방식으로 키를 생성하고 있습니다.

private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

이 방식은 서버 실행 시마다 랜덤한 키를 동적으로 생성하는 방식인것 같습니다.
서버를 재시작하면 이전에 발급한 JWT를 검증할 수 없습니다. 기존 토큰의 서명 검증이 실패해서 사용자 인증이 무조건 실패하게 됩니다. JWT는 발급과 검증에 동일한 Secret Key를 사용해야 합니다.

따라서 Secret Key를 외부 설정으로 고정해서 관리해주세요.
Secret Key는 절대 application.yml 코드에 하드코딩하지 말고 반드시 .env 파일에 작성 후 불러오는 방식으로 안전한 외부 설정으로 관리해주세요.

JWT_SECRET=your-secret-key

application.yml에서 환경변수로부터 값을 읽어서 사용해주세요

jwt:
  secret: ${JWT_SECRET}

그 후 코드에선 예를 들면 이런식으로 둘 중 하나로 값을 주입해 사용해서 작업 해주세요.

@ConfigurationProperties(prefix = "jwt") // 또는
@Value("${jwt.secret}") 

또한 JWT 인증 설계 시에는 Access TokenRefresh Token을 분리해서 각각의 만료 시간, 사용 목적에 맞게 발급, 관리하는 것이 보안상 안전합니다.

  • Access Token: 짧은 만료시간(예: 1시간~3시간)으로 설정해 사용자 인증용으로만 사용
  • Refresh Token: 더 긴 만료시간(예: 2주~한 달)으로 설정해 Access Token 재발급용으로 사용

각 토큰의 만료기간을 한번 고민해보고 고려하여 application.yml에 별도의 설정값으로 분리해서 관리해 주세요.

    jwt:
      secret: ${JWT_SECRET}
      access-token: 3600  # 초 단위 (예: 1시간)
      refresh-token: 1209600  # 초 단위 (예: 2주)

// then
User foundUser = userRepository.findByUsername(TEST_USERNAME).orElseThrow();

System.out.println("서비스 반환 User 객체:");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

System.out.println() 기반의 테스트 출력이 너무 많습니다.
테스트 코드에서는 가급적 불필요한 출력문은 줄여주시고 검증(assert 등) 코드 위주로 작성해 주세요.

디버깅이나 로그가 꼭 필요한 경우에는 System.out.println() 대신 Logger(sl4fj 등)를 사용해 주세요.

e56e5da 커밋의 리팩토링 과정에서 Controller, jwtutil 에서 username 기반을 email  기반으로 수정하는 과정이 누락된 것을 보충
cf8459b 커밋에서 테스트코드 수정이 누락 됨에 따라 발생한 오류 수정 및 테스트 코드 리팩토링(이메일, 비밀번호 없음에 대응하는 예외처리 테스트)
… 구조변경

aec9782 에서 작성한 estControllerAdvice + enum 의 구조를 전역적으로 리팩토링 및 이에따른 테스트 코드 및 컨트롤러, 서비스 등에 반영
CommonResponse 의 첫번째 필드를 error(오류 여부) 가 아닌 success(실행 성공 여부) 로 수정
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐯 준진 👾 feat 새 기능 / New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 회원가입, 로그인, 유저정보 수정 기능 구현

4 participants