거의 모든 웹 애플리케이션의 시작인 회원가입 로그인 중 회원가입을 구현해 보려 한다.
1. 개발환경
기본적으로 스프링부트 3, 자바는 17을 사용중이다.
데이터 베이스는 MySQL
JPA를 사용해 따로 SQL문을 작성하지 않고 작업하고 있다.
카테고리 | 도구/기술 |
프로그래밍 언어 | Java 17 |
프레임워크 | Spring Boot 3.4.2 |
빌드 도구 | gradle |
데이터베이스 | MySQL |
ORM 도구 | Spring Data JPA |
IDE | IntelliJ |
버전 관리 | Git, GitHub, GitLab |
패키지 관리 | Maven Central |
API 문서화 | Swagger |
로깅 | SLF4J |
2. build.gradle
의존성은 기본적인 프로젝트 실행을 위한 web starter와, mysql, jpa, 비밀번호 암호화를 위한 security 그리고 편의를 위해 lombok 정도를 사용한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// Security - BCryptPasswordEncoder 사용
implementation 'org.springframework.boot:spring-boot-starter-security'
// MySQL db
runtimeOnly 'com.mysql:mysql-connector-j'
// JPA 사용
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Web 스타터 - 아파치 톰켓, Web MVC, JSON 직렬화&역직렬화, validation
implementation 'org.springframework.boot:spring-boot-starter-web'
// getter, setter 등의 어노테이션
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// 테스트 관련
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
3. 디렉토리 구조
디렉토리 구조는 기본적으로 DDD(Domain-Driven Design, 도메인 주도 설계)을 따라가려 했다.
📂 src
├── 📂 main
│ ├── 📂 java
│ │ └── 📂 com.example.springboot3template
│ │ ├── 📄 Springboot3templateApplication.java
│ │ │
│ │ ├── 📂 auth
│ │ │ ├── 📂 application # 🌟 애플리케이션 서비스 계층
│ │ │ │ ├── 📂 dto
│ │ │ │ │ ├── 📂 mapper
│ │ │ │ │ ├── 📂 req
│ │ │ │ │ │ └── 📄 SignUpReq.java
│ │ │ │ │ ├── 📂 res
│ │ │ │ └── 📂 service
│ │ │ │ └── 📄 AuthService.java
│ │ │ │
│ │ │ ├── 📂 domain # 🌟 도메인 계층
│ │ │ │ ├── 📂 entity
│ │ │ │ │ ├── 📄 User.java
│ │ │ │ │ ├── 📄 UserRoleEnum.java
│ │ │ │
│ │ │ ├── 📂 infrastructure # 🌟 인프라 계층 (데이터 저장소)
│ │ │ │ ├── 📂 repository
│ │ │ │ │ └── 📄 UserRepository.java
│ │ │ │
│ │ │ ├── 📂 presentation # 🌟 프레젠테이션 계층
│ │ │ │ └── 📂 controller
│ │ │ │ └── 📄 AuthController.java
│ │ │
│ │ ├── 📂 common
│ │ │ └── 📂 security
│ │ │ └── 📄 SecurityConfig.java
│ │
│ ├── 📂 resources
│ │ ├── 📄 application-local.yml
│ │ ├── 📄 application.yml
4. 구현 순서
entity -> repository -> controller -> dto -> service
(일반적인 개발순서는 다음( entity -> repository -> dto -> service -> controller )과 같으나 난 controller 부터 개발하는게 편해 취향 차이인것 같다. )
5. 코드
1. User 엔티티
데이터베이스 테이블과 매핑되는 객체로 각 필드들은 db의 칼럼과 매칭된다.
@Id : 모든 엔티티는 하나의 고유 식별자를 갖어야 한다.
@Entity
@Table(name = "user_info_tb")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@Column
private String username;
@Column
private String password;
@Column
@Enumerated(EnumType.STRING)
private UserRoleEnum role;
}
2. 역할 enum
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
3. repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
4. controller
@RequestMapping("/api/v1/auth")
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@RequestBody SignUpReq req) {
authService.signUp(req);
return ResponseEntity.ok().build();
}
}
5. dto (SignUpReq)
@Data
public class SignUpReq {
private String username;
private String password;
private UserRoleEnum role;
private String adminToken;
}
6. service
@Service
@RequiredArgsConstructor
public class AuthService {
private static final String ADMIN_TOKEN = "adminToken";
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public void signUp(SignUpReq req) {
String username = req.getUsername();
String password = passwordEncoder.encode(req.getPassword());
UserRoleEnum role = req.getRole();
// 회원 중복 확인
Optional<User> checkUsername = userRepository.findByUsername(username);
if (checkUsername.isPresent()) {
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}
if(role == null) {
role = UserRoleEnum.USER;
} else if(role == UserRoleEnum.ADMIN) {
if(req.getAdminToken() == null || !req.getAdminToken().equals(ADMIN_TOKEN)) {
throw new IllegalArgumentException("유효한 관리자 토큰이 필요합니다.");
}
}
// 사용자 등록
User user = User.builder()
.username(username)
.password(password)
.role(role)
.build();
userRepository.save(user);
}
}
7. securityconfig
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// Session 비활성화
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 요청 권한 설정
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
// api 요청 허가
.requestMatchers("/api/v1/auth/**").permitAll() // 회원가입 로그인은 필터 통과
// HTML 페이지 접근을 인증 없이 허용
// .requestMatchers("/").permitAll()
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
return http.build();
}
}
'Spring Boot > Sprng Boot Sample Code' 카테고리의 다른 글
Spring Boot 에서 네이버 클라우드 Object Storage 에 파일 업로드 (1) | 2025.03.03 |
---|---|
[SpringBoot 3.x] 4. @PreAuthorize 어노테이션을 이용한 API 인가 설정 (0) | 2025.03.03 |
[Spring Boot 3.x] 3. JWT 토큰 인증(filter) & API 테스트 (0) | 2025.03.01 |
[Spring Boot 3.x] 2. JWT 토큰과 쿠키를 이용한 로그인 구현 (0) | 2025.02.24 |
build.gradle 이해하기 (1) | 2025.01.30 |