본문 바로가기
💻 뚝딱뚝딱/북북클럽

[개발일지#001] 회원 도메인(User) 개발

by 뚜루리 2025. 4. 18.
728x90
320x100

🎯 오늘의 목표

  • 회원(User) 도메인 개발

⚙️ 진행한 작업

  • 회원(User) 엔티티 생성
  • 회원(User) 레파지토리 생성
  • 회원(User)  서비스 생성

🛠️ 개발내용

📌 회원(User) 엔티티 생성

package ddururi.bookbookclub.domain.user.entity;

import ddururi.bookbookclub.domain.user.enums.Role;
import ddururi.bookbookclub.domain.user.enums.UserStatus;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class User {


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

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Setter
    (unique = true, nullable = false)
    private String nickname;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @Setter
    @Column(length = 500)
    private String bio;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // 정적 생성 메서드
    public static User create(String email, String encodedPassword, String nickname) {
        User user = new User();
        user.email = email;
        user.password = encodedPassword;
        user.nickname = nickname;
        user.role = Role.USER;
        user.status = UserStatus.ACTIVE;
        return user;
    }
}
  • 롬복을 사용하여 클래스 전체에 @Getter를 주고 변경 가능한 필드인 닉네임, 자기소개 컬럼에만 @Setter를 줌.
  • Auditing 기능을 사용하여 생성날짜, 마지막 저장날짜를 자동으로 저장.
  • 정적 생성 메서드를 만들어, 불변성 보장 + 생성 책임을 명확히 함.
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • 파라미터가 없는 기본 생성자를 protected 접근제한자로 지정하여 외부에서 new User() 하지 못하게 막아줌.

 

🙋‍♀️ 왜 protected 생성자를 쓰는 걸까?

이유 설명
JPA 필요로 JPA 리플렉션으로 객체를 생성하기 때문에 기본 생성자 필수
객체 무분별한 생성 방지 new User() 아무 없이 객체 생성되는 방지하고, Builder 생성자만으로 생성하게 유도
도메인 무결성 유지 필수 없이 생성되면 되는 엔티티에서 실수 방지 가능

 

📌 Auditing 기능 (정의, 사용법, 사용하는 이유 등)

 

[Spring/JPA] Auditing 기능이란? (정의, 사용법, 사용하는 이유 등)

Auditing 기능은 Spring Data JPA에서 누가 언제 어떤 작업을 했는지를 자동으로 기록해주는 기능.(즉, 어떤 사람이 게시글을 생성하거나 수정했을 때, 그 생성 일시, 수정 일시, 작성자, 수정자 같은 정

ddururiiiiiii.tistory.com

 

📌  @Builder란? (Feat.정적 생성 메서드)

 

[Spring] @Builder란? (Feat. 정적 생성 메서드)

@Builder는 롬복(Lombok)에서 제공하는 애노테이션 중 하나로,복잡한 객체를 간편하고 가독성 좋게 생성할 수 있도록 도와줌. @Builder를 사용하는 이유?[기존방식]User user = new User("홍길동", "hello@naver.co

ddururiiiiiii.tistory.com

 

📌 회원(User) 엔티티 와 연관된 enum 클래스 생성

1. Role

package ddururi.bookbookclub.domain.user.enums;


import lombok.Getter;

@Getter
public enum Role {

    USER("일반 사용자"),
    ADMIN("관리자");

    private final String description;

    Role(String description) {
        this.description = description;
    }

}

 

2. UserStatus

package ddururi.bookbookclub.domain.user.enums;

import lombok.Getter;

@Getter
public enum UserStatus {
    ACTIVE("활동 중"),
    WITHDRAWN("탈퇴");

    private final String description;

    UserStatus(String description) {
        this.description = description;
    }
}

 

 

 


 

📌 회원(User) 레파지토리 생성

package ddururi.bookbookclub.domain.user.repository;

import ddururi.bookbookclub.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    //이메일로 사용자 찾기
    Optional<User> findByEmail(String email);

    //이메일 중복 여부 확인 (회원가입 시)
    boolean existsByEmail(String email);
}
  • JPA Repository를 사용해서 보다 간결하게 작성

 

 

📌  JPA Repository? (정의, 구조, 사용하는 이유, 사용법 )

 

[Spring JPA]JPA Repository란? (정의, 구조, 사용하는 이유, 사용법 등)

🌱 JPA Repository란?JPA Entity 객체를 데이터베이스 테이블과 매핑해서, 이를 기반으로 자동으로 DB 쿼리를 처리해주는 인터페이스임! 🧱 기본 구조public interface UserRepository extends JpaRepository { } User :

ddururiiiiiii.tistory.com

 

 


📌 회원(User) 서비스 생성

package ddururi.bookbookclub.domain.user.service;


import ddururi.bookbookclub.domain.user.dto.UserLoginRequest;
import ddururi.bookbookclub.domain.user.dto.UserResponse;
import ddururi.bookbookclub.domain.user.dto.UserSignupRequest;
import ddururi.bookbookclub.domain.user.dto.UserUpdateRequest;
import ddururi.bookbookclub.domain.user.entity.User;
import ddururi.bookbookclub.domain.user.repository.UserRepository;
import ddururi.bookbookclub.global.exception.DuplicateEmailException;
import ddururi.bookbookclub.global.exception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    //회원가입
    public UserResponse signup(UserSignupRequest request){
        validateDuplicateEmail(request.getEmail());

        User user = User.create(
                request.getEmail(),
                passwordEncoder.encode(request.getPassword()),
                request.getNickname()
        );

        userRepository.save(user);

        return UserResponse.from(user);
    }
    
    //이메일 중복 확인
    private void validateDuplicateEmail(String email) {
        if (userRepository.existsByEmail(email)) {
            throw new DuplicateEmailException();
        }
    }
    //로그인
    public UserResponse login(UserLoginRequest request){
        User user = validateUserLogin(request.getEmail(), request.getPassword());
        return UserResponse.from(user);
    }
    
    //비밀번호 확인
    private User validateUserLogin(String email, String rawPassword) {
        User user = userRepository.findByEmail(email)
                .orElseThrow(UserNotFoundException::new);

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

        return user;
    }

    // 회원정보 수정 (닉네임, 자기소개)
    @Transactional
    public void updateProfile(Long userId, UserUpdateRequest request) {
        User user = userRepository.findById(userId)
                .orElseThrow(UserNotFoundException::new);

        user.setNickname(request.getNickname());
        user.setBio(request.getBio());
        // save 호출 안 해도 됨 → JPA가 변경감지로 UPDATE 수행
    }
}
  • validateDuplicateEmail(), validateUserLogin() 등 유효성 검사하는 메서드를 따로 만들어서 조금 더 간결하게 작성.

 

📌 스프링 시큐리티 - 비밀번호 암호화

 

[스프링 시큐리티] 비밀번호 암호화 하기 (BCryptPasswordEncoder)

1. 비밀번호 암호화란?비밀번호 **암호화(Encryption)**는 사용자의 비밀번호를 데이터베이스에 저장할 때 읽을 수 없는 형태로 바꾸는 것.하지만 정확히 말하면, 우리가 흔히 사용하는 방식은 암호

ddururiiiiiii.tistory.com

 

📌 BCryptPasswordEncoder 란?

 

[Spring Security] BCryptPasswordEncoder란? (정의, 사용하는 이유, 사용법 등)

🔐 BCryptPasswordEncoder란?Spring Security에서 제공하는 비밀번호 해싱 클래스.즉, 회원가입 시 비밀번호를 안전하게 저장하고, 로그인 시 비밀번호를 안전하게 검증하기 위해 사용하는 암호화 도구비

ddururiiiiiii.tistory.com

 

📌 회원정보 수정 메서드에만 @Transactional 어노테이션을 붙인 이유는?

  • JPA는 트랜잭션 안에서 user.setNickname() 이런 필드 변경을 감지하고 메서드가 끝나면서 트랜잭션이 커밋될 때 → 자동으로 update 쿼리를 날려주는데  트랜잭션이 없으면 변경을 감지해도 실제 DB에 반영되지 않음 그래서 update, delete 작업할 무조건 @Transactional 붙여줘야 함.

 

📌  save() @Transactional 없어도 되는 걸까?

  • Spring Data JPA의 save() 자체가 내부적으로 트랜잭션을 지원해서 안 붙여줘도 됨.

 

📌  예외적으로 꼭 붙여야 하는 경우는?

1. save + 다른 작업이 함께 있는 경우

@Transactional
public void register(User user) {
    userRepository.save(user);
    emailService.sendWelcomeEmail(user.getEmail());

}
  • 이메일 보내다가 에러 나면 전체 롤백되어야 하기 때문에 이럴 땐 묶어서 트랜잭션 처리해야 하므로 @Transactional 필요

 

2. 변경 감지(update), delete, lazy loading 등은 반드시 필요

@Transactional
public void updateUser(Long id) {
    User user = userRepository.findById(id).get();
    user.setNickname("변경됨");
    // 트랜잭션 끝날 때 update 쿼리 발생
}
  • 트랜잭션 없으면변경 감지를 못함 → update 쿼리 날라감

 

 


 

📌 회원(User) 서비스 관련 DTO 생성

1. UserLoginRequest

package ddururi.bookbookclub.domain.user.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginRequest {
    private String email;
    private String password;
}

 

2. UserSignupRequest

package ddururi.bookbookclub.domain.user.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserSignupRequest {
    private String email;
    private String password;
    private String nickname;
}

 

 

3. UserUpdateRequest

package ddururi.bookbookclub.domain.user.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserUpdateRequest {
    private String nickname;
    private String bio;
}

 

4. UserResponse

package ddururi.bookbookclub.domain.user.dto;

import ddururi.bookbookclub.domain.user.entity.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class UserResponse {
    private Long id;
    private String email;
    private String nickname;
    private String role;

    public static UserResponse from(User user) {
        return UserResponse.builder()
                .id(user.getId())
                .email(user.getEmail())
                .nickname(user.getNickname())
                .role(user.getRole().name())
                .build();
    }
}
  • 정적 생성 메서드 + @Builder 조합 사용

 

📌 @Builder, 정적 생성 메서드 ?

 

[Spring] @Builder란? (Feat. 정적 생성 메서드)

@Builder는 롬복(Lombok)에서 제공하는 애노테이션 중 하나로,복잡한 객체를 간편하고 가독성 좋게 생성할 수 있도록 도와줌. @Builder를 사용하는 이유?[기존방식]User user = new User("홍길동", "hello@naver.co

ddururiiiiiii.tistory.com

 


📌 회원(User) 서비스 관련 예외 클래스 생성

1. DuplicateEmailException

package ddururi.bookbookclub.global.exception;

public class DuplicateEmailException extends RuntimeException {
    public DuplicateEmailException() {
        super("이미 가입된 이메일입니다.");
    }
}

 

2. UserNotFoundException

package ddururi.bookbookclub.global.exception;

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException() {
        super("사용자를 찾을 수 없습니다.");
    }
}

 

 


 

📌 회원(User) 서비스 관련 비밀번호 암호화 설정 클래스 생성

1 . SecurityConfig

package ddururi.bookbookclub.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
728x90
320x100