본문 바로가기
카테고리 없음

사용자 생성 기능 코드도 만들어줘 springboot 3 코드만 있으면 될 것 같아

by 두잇건강 2025. 8. 20.
반응형

알겠습니다. 앞서 제공했던 구조(스프링 부트 3 + 스프링 시큐리티 6 + JWT, 기존 User/Authority 엔티티, H2 또는 MariaDB 사용, PasswordEncoder, Security 설정, 컨트롤러/서비스/리포지토리 분리)를 그대로 준수하면서 “회원가입(사용자 생성)” 코드를 깨끗하게 다시 정리해 드립니다.

아래 코드는 다음을 충족합니다:

  • Spring Boot 3, Security 6 방식(Lambda DSL, jakarta.*)
  • PasswordEncoder(BCrypt)로 비밀번호 암호화
  • 이메일 중복 검사
  • 기본 권한 ROLE_USER 자동 부여
  • DTO 검증(@Valid)
  • 서비스 계층 트랜잭션
  • Security에서 회원가입 엔드포인트 permitAll
  • 앞서 답변해 드린 User/Authority 구조와 호환

프로젝트 패키지 예시

  • com.example.jwt
    • config
    • user
      • controller
      • dto
      • entity
      • repository
      • service

1) DTO

CreateUserRequest.java

package com.example.jwt.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CreateUserRequest(
        @NotBlank @Email String email,
        @NotBlank @Size(min = 8, max = 100) String password,
        @NotBlank @Size(min = 2, max = 50) String name,
        String phone
) {}

UserResponse.java

package com.example.jwt.user.dto;

import java.time.LocalDateTime;
import java.util.Set;

public record UserResponse(
        Long id,
        String email,
        String name,
        String phone,
        Set roles,
        LocalDateTime createdDate
) {}

2) Entity

Authority.java

package com.example.jwt.user.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "authority")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Authority {
    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName;

    @Column(length = 255)
    private String description;
}

User.java

package com.example.jwt.user.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;
import java.util.Set;

@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

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

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

    @Column(length = 20)
    private String phone;

    @Column(name = "created_date", nullable = false)
    private LocalDateTime createdDate;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_authority",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")}
    )
    private Set authorities;

    @PrePersist
    public void onCreate() {
        if (createdDate == null) createdDate = LocalDateTime.now();
    }
}

3) Repository

AuthorityRepository.java

package com.example.jwt.user.repository;

import com.example.jwt.user.entity.Authority;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorityRepository extends JpaRepository {
}

UserRepository.java

package com.example.jwt.user.repository;

import com.example.jwt.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository {
    boolean existsByEmail(String email);
    Optional findByEmail(String email);
}

4) Service

UserService.java

package com.example.jwt.user.service;

import com.example.jwt.user.dto.CreateUserRequest;
import com.example.jwt.user.dto.UserResponse;

public interface UserService {
    UserResponse createUser(CreateUserRequest request);
}

UserServiceImpl.java

package com.example.jwt.user.service;

import com.example.jwt.user.dto.CreateUserRequest;
import com.example.jwt.user.dto.UserResponse;
import com.example.jwt.user.entity.Authority;
import com.example.jwt.user.entity.User;
import com.example.jwt.user.repository.AuthorityRepository;
import com.example.jwt.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final AuthorityRepository authorityRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserResponse createUser(CreateUserRequest request) {
        // 이메일 중복 체크
        if (userRepository.existsByEmail(request.email())) {
            throw new IllegalArgumentException("이미 사용 중인 이메일입니다.");
        }

        // 기본 권한 확보(없으면 생성)
        Authority roleUser = authorityRepository.findById("ROLE_USER")
                .orElseGet(() -> authorityRepository.save(
                        Authority.builder()
                                .authorityName("ROLE_USER")
                                .description("일반 사용자 권한")
                                .build()
                ));

        // 비밀번호 암호화
        String encodedPw = passwordEncoder.encode(request.password());

        // 엔티티 생성
        User user = User.builder()
                .email(request.email())
                .password(encodedPw)
                .name(request.name())
                .phone(request.phone())
                .authorities(Set.of(roleUser))
                .build();

        // 저장
        User saved = userRepository.save(user);

        // 응답 변환
        return new UserResponse(
                saved.getId(),
                saved.getEmail(),
                saved.getName(),
                saved.getPhone(),
                saved.getAuthorities().stream()
                        .map(Authority::getAuthorityName)
                        .collect(Collectors.toSet()),
                saved.getCreatedDate()
        );
    }
}

5) Controller

UserController.java

package com.example.jwt.user.controller;

import com.example.jwt.user.dto.CreateUserRequest;
import com.example.jwt.user.dto.UserResponse;
import com.example.jwt.user.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {

    private final UserService userService;

    // 회원가입 (비인증 접근 허용)
    @PostMapping
    public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) {
        return ResponseEntity.ok(userService.createUser(request));
    }
}

6) Security 설정

SecurityConfig.java

package com.example.jwt.config;

import com.example.jwt.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.*;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception {
        return cfg.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .cors(c -> c.configurationSource(corsConfigurationSource()))
            .headers(h -> h.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .formLogin(AbstractHttpConfigurer::disable)
            .httpBasic(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                    // 회원가입/로그인/H2 콘솔 허용
                    .requestMatchers("/api/users", "/api/authenticate", "/h2-console/**").permitAll()
                    .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        var cfg = new CorsConfiguration();
        cfg.setAllowedOriginPatterns(Arrays.asList("*"));
        cfg.setAllowedMethods(Arrays.asList("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
        cfg.setAllowedHeaders(Arrays.asList("*"));
        cfg.setAllowCredentials(true);
        var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", cfg);
        return source;
    }
}

7) 예외 처리(선택)

GlobalExceptionHandler.java

package com.example.jwt.common;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
                .findFirst()
                .map(err -> err.getField() + " " + err.getDefaultMessage())
                .orElse("유효성 검증 실패");
        return ResponseEntity.badRequest().body(Map.of("message", msg));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity handleOther(Exception e) {
        return ResponseEntity.internalServerError().body(Map.of("message", "서버 오류가 발생했습니다."));
    }
}

8) application.yml 예시(H2 메모리로 빠르게 테스트)

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

# CORS, 서버 포트 등 필요시 추가
server:
  port: 8080

9) 요청/응답 예시

요청
POST /api/users

{
  "email": "user1@example.com",
  "password": "Password123!",
  "name": "홍길동",
  "phone": "010-1234-5678"
}

성공 응답

{
  "id": 1,
  "email": "user1@example.com",
  "name": "홍길동",
  "phone": "010-1234-5678",
  "roles": ["ROLE_USER"],
  "createdDate": "2025-08-18T10:30:25.123"
}

구현 체크포인트

  • 엔티티 이름/필드(users, authority, user_authority)는 앞서 만든 테이블/엔티티와 일치합니다.
  • PasswordEncoder를 반드시 빈으로 등록해 BCrypt 해싱 사용.
  • SecurityConfig에서 /api/users(POST)와 /api/authenticate는 permitAll.
  • Authority “ROLE_USER”가 없으면 회원가입 시 자동 생성.
  • Service 계층에서 트랜잭션 처리와 검증 예외(IllegalArgumentException) 명확화.
  • DTO(@Valid)로 요청 검증.

MariaDB를 사용할 경우

  • datasource URL/드라이버 교체
  • 앞서 제공한 DDL(users, authority, user_authority)을 사용하거나 JPA ddl-auto=update로 생성

혹시 기존 프로젝트의 패키지/클래스명이 다르다면, import/패키지 경로만 맞춰 붙여 넣으면 바로 동작합니다. 필요한 경우 이메일 인증(토큰 발송), 추가 유효성 검사(정규식), 관리자 생성/권한 부여, 응답 마스킹 등도 확장해 드릴 수 있습니다.

반응형