후라이

[Web 게시판] 회원 관리 설계 | User 엔티티 설계와 비즈니스 로직 본문

Spring/웹 게시판

[Web 게시판] 회원 관리 설계 | User 엔티티 설계와 비즈니스 로직

힐안 2025. 1. 18. 10:21
개발 환경
SpringBoot 3.4.1
Gradle - Groovy
JPA(Spring Data JPA)
Spring Security
Lombok
thymeleaf
DB : MySQL

 

https://github.com/gmlfks/board

 

GitHub - gmlfks/board: inital commit

inital commit. Contribute to gmlfks/board development by creating an account on GitHub.

github.com

 


1. DB 설계 

 

 

  • user_id : PRIMARY KEY
  • username : 회원 이름 (아이디)
  • password : 비밀번호 8~16자 영문 대 소문자, 숫자, 특수문자
  • nickname
  • email : 메일 형식
  • role : User / Admin
  • created_date
  • modified_date

 

2. Entity

 

@Getter
@Setter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User extends BaseTimeEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    private String username;

    private String nickname;

    private String password;

    private String email;

    @Enumerated(EnumType.STRING)
    private Role role;

    public void modify(String username, String email, String nickname) {
        this.username = username;
        this.nickname = nickname;
        this.email = email;

    }

    public User updateModifiedDate() {
        this.onPreUpdate();
        return this;
    }

    public String getRoleValue() {
        return this.role.getValue();
    }
}

 

BaseTimeEntity를 상속 받도록 합니다.

BaseTimeEntity는 기본 시간 속성을 관리하도록 하는 추상클래스입니다. (Auditing Base Entity, Base Time Enitity)

@CreatedDate와 @LastModifiedDate 어노테이션을 활용해 엔티티의 생성 및 수정 시점을 자동으로 기록하도록 설계했습니다.

코드는 아래에 첨부합니다. :)

 

DB에서 설계한 것과 동일한 속성들을 사용하고, Role의 경우 Enum 클래스로 간단하게 구현하였습니다.

modify(), updateModifiedDate(), getRoleValue() 메소드의 경우 Service 계층에서 사용할 것이므로 그 때 다루도록 하겠습니다.

 

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
abstract class BaseTimeEntity {

    @Column(name = "created_date")
    @CreatedDate
    private String createdDate;

    @Column(name = "modified_date")
    @LastModifiedDate
    private String modifiedDate;

    /* 해당 엔티티를 저장하기 이전에 실행 */
    @PrePersist
    public void onPrePersis() {
        this.createdDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
        this.modifiedDate = this.createdDate;
    }

    /* 해당 엔티티를 업데이트 하기 이전에 실행 */
    @PreUpdate
    public void onPreUpdate() {
        this.modifiedDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm"));
    }


}

 

일반적으로 LocalDateTime가 같은 날짜/시간 전용 클래스를 사용하지만

단순한 텍스트 표현만 필요할 예정이라 String으로 선언한 후 포맷팅을 해주었습니다.

 

3. Repository

 

Repository는 JpaRepository를 상속 받기 때문에 별다른 로직이 존재하진 않습니다.

하지만 Login/Logout을 구현하면서 필요한 Spring security나 유효성 검사에서 사용할 메소드들이 점차 추가될 것입니다.

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

    Optional<User> findByUsername(String username); //Security

    boolean existsByUsername(String username); //중복성 검사 시 사용
    boolean existsByNickname(String nickname); //중복성 검사 시 사용
    boolean existsByEmail(String email); //중복성 검사 시 사용
    
    User findByNickname(String nickname); // Post에서 게시글 찾을 때 사용

}

 

4. Service

 

이제 본격적인 비즈니스 로직을 구현해봅시다.

먼저 기본 CRUD를 구현할 것입니다. 단, 아직 회원 DELETE는 포함시키지 않았습니다. 

 

4.1) Create - 회원가입

 

회원가입 비즈니스 로직은 간단합니다. 요청으로 들어온 userDto 객체에 대해 중복 검사를 한 후,

중복되지 않으면 repository.save()하면 됩니다.

    @Transactional
    public void joinUser(UserDto.Request user) {

        boolean isUser = userRepository.existsByUsername(user.getUsername());
        if(isUser) {
            return;
        }
        user.setPassword(passwordEncoder.encode(user.getPassword()));

        userRepository.save(user.toEntity());
    }

 

리포지토리에 이미 똑같은 username이 존재할 경우엔 메서드를 종료하고, 그렇지 않은 경우엔 

password를 암호화하고 리포지토리에 회원 정보를 저장합니다.

 

user.toEntity()는 userDto.Request 객체를 User 엔티티로 변환하는 메서드입니다.

클라이언트에서 전달된 사용자 입력 데이터를 담고 있는 DTO 객체를 데이터베이스에 저장할 수 있도록 엔티티 객체로 변환해야 합니다.

 

4.2) Read - 회원 조회

 

회원 조회 비즈니스 로직 또한 간단합니다.

파라미터로 넘어온 user id를 통해 User를 식별하고 응답하면 됩니다.

    @Transactional
    public UserDto.Response viewUser(Long userId) {
        User user = userRepository.findById(userId).orElseThrow(()-> new RuntimeException("User is not found"));
        return new UserDto.Response(user);
    }

 

4.3) Update - 회원 정보 수정 

 

수정 부분은 사실 username과 password를 변경 가능하도록 만들고 싶었는데 세션 변경과 관련하여 아직 테스트를 진행하지 못해

우선 username과 email, nickname을 변경하도록 설계했습니다. 이 부분은 추후 수정할 예정입니다.

@Transactional
    public void modify(UserDto.Request dto) {
        User user = userRepository.findById(dto.getId()).orElseThrow(()->
                new IllegalArgumentException("해당 회원이 존재하지 않습니다."));

        //String encPassword = passwordEncoder.encode(dto.getPassword());
        user.modify(dto.getUsername(),dto.getEmail(), dto.getNickname());
    }

 

 

5. Controller - 회원 가입 / 회원 수정

 

로그인/로그아웃과 관련하여 Spring security에 대한 내용은 추후에 다룰 예정이므로

간단하게 회원가입과 회원 정보 수정 controller만 다뤄보겠습니다.

 

<회원 가입>

 

- @GetMapping 으로 회원가입 폼을 연결합니다

 

@GetMapping("/auth/join")
    public String joinForm() {
        return "/user/joinForm";
    }

 

 

- @PostMapping으로 실제 회원 가입 로직을 연결합니다.

여기서, @Valid UserDto.Request dto 는 해당 dto 객체에 대한 유효성 검사를 수행하도록 합니다.

이 부분은 아래 링크를 참고하시면 좋을 것 같습니다. 간단히 말해 검증 규칙을 기준으로 검증을 진행하는 과정입니다.

 

https://gaeran.tistory.com/52

 

@Valid 어노테이션을 사용해 유효성 검사하기

이번 게시물에서는 스프링에서 @Valid를 사용하여 검증하는 방법에 대해 소개합니다. 1. @Valid @Valid 어노테이션은 JAVA 표준 스펙의 일부로, Bean Validation API(JSR 303/JSR 380)에 포함된 것으로 Spring Framew

gaeran.tistory.com

 

그리고, 검증 결과가 errors 객체에 담기게 되며 만약 검증에 실패한 필드가 있으면, errors 객체에 오류 메시지가 추가되도록 구현합니다.

 

    @PostMapping("/auth/joinPro")
    public String saveUser(@Valid UserDto.Request dto, Errors errors, Model model) {

        if(errors.hasErrors()) {
            model.addAttribute("user",dto);

            Map<String,String> validateResult = userService.validateHandling(errors);
            for(String key : validateResult.keySet()) {
                model.addAttribute(key, validateResult.get(key));
            }
            return "/user/joinForm";
        }

        userService.joinUser(dto);

        model.addAttribute("message", "회원가입이 완료되었습니다.");
        model.addAttribute("searchUrl", "/");
        return "message";
    }

 

if문에 해당하는 내용은 만약 errors가 존재하면 (사용자가 입력한 데이터가 유효하지 않으면)

현재 입력된 dto 객체를 모델에 추가하여, 검증 오류가 발생해도 사용자 입력을 다시 화면에 표시할 수 있도록 합니다.

 

이게 무슨 말이냐면

만약 회원가입 중에 이메일 형식이 맞지 않는 이메일을 입력한다거나, 비밀번호 자릿수가 부족하다는 등

검증 요건에 맞지 않은 입력값을 회원 가입 input으로 입력할 경우

해당 에러 메시지를 사용자에게 보여준다는 뜻입니다.

 

 

검증 오류 처리와 관련하여 validateHandling(errors) 또한 UserService내 메서드로 구현했습니다.

해당 내용 또한 다른 게시물에서 다룰 예정이므로 첨부한 github 주소를 참고해서 코드를 살펴봐주시면 됩니다!

 

결론적으로 오류가 있을 경우 회원가입 폼으로 다시 돌아가 오류 메시지를 표시합니다.

오류없이 모든 유효성 검사를 끝마친 회원 정보는 정상적으로 userService.joinUser(dto)를 통해 회원가입이 이루어집니다.

저는 회원가입이 완료되었다는 성공 메시지를 띄우고 싶어 "message.html" 뷰를 구현하였습니다.

회원가입이 완료되면 메인 페이지("/)로 URL을 이동합니다.

 

 

<회원 수정>

 

@GetMapping("/user/modify")
    public String modifyUser(@LoginUser UserDto.Response user, Model model) {
        if(user!=null) model.addAttribute("user",user);
        return "/user/user-modify";
    }

 

회원 가입 때와 마찬가지로 일단 회원 정보 수정하는 폼으로 @GetMapping하게 합니다.

그리고 RESTful API 방식으로 사용자 정보 수정을 처리하는 컨트롤러를 따로 만듭니다. 

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserApiController {
    private final UserService userService;
    private final AuthenticationManager authenticationManager;

    @PostMapping("/user")
    public ResponseEntity<String> modify(@RequestBody UserDto.Request dto) {
        userService.modify(dto);

        /* 변경된 세션 등록 */
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(dto.getUsername(),dto.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        return new ResponseEntity<>("success", HttpStatus.OK);
    }
}

 

HTTP POST 요청을 통해 사용자 정보를 수정하고, 인증 세션을 갱신합니다.

@RestController를 통해 해당 클래스 내의 메서드들이 JSON 형식으로 응답을 반환하게 합니다.

즉, 반환되는 객체가 HTTP 응답 본문에 직접 작성되겠죠??

 

modify 메서드를 통해 사용자 정보 수정에 필요한 데이터를 담은 정보가 JSON 데이터로 수신됩니다.

@RequestBody : HTTP 요청 본문에 있는 JSON 데이터를 UserDto.Request 객체로 자동 변환합니다.

ResponseEntity<String> : ResponseEntity 객체를 반환합니다. 해당 메서드의 결과 "success"라는 문자열과 함께 200 상태 코드를 반환하게 됩니다.

 

AuthenticationManager.authenticate()의도한 바로는 사용자가 제출한 username과 password로 새로운 인증 객체를 생성해 Spring Security에서 사용자 인증을 관리하도록(자격 증명이 올바른지 확인) 설계했습니다.

사실 이 부분이 제대로 동작하진 않습니다.

저는 회원 정보 수정을 username, email, nickname으로 설정했으므로, 이 부분은 일단 코드 작성만 해놓은 것입니다.

 

SecurityContextHolder.getContext().setAuthentication(authentication) : SecurityContextHolder 자체는 Spring Security에서의 현재 보안 컨텍스트를 관리하는 클래스입니다. 여기서 getContext() 메서드를 통해 현재의 보안 컨텍스트를 가져오고,

setAuthentication을 통해 인증 정보를 세션에 갱신하게 됩니다.

 

즉, 사용자가 정보를 수정한 후, 새로운 인증 정보를 세션에 저장하여 이후 요청에서 해당 사용자가 인증된 상태로 유지될 수 있게 합니다.