Service 패키지
이제는 Service 패키지를 구현할 차례이다.
Service패키지는 아래와 같이 구성하였다.

서비스 계층은 비즈니스 로직을 캡슐화하고, 트랜잭션 경계를 관리하며, 컨트롤러와 데이터 접근 계층(Repository) 사이에서 책임을 분리하는 역할을 한다.
회원가입 기능에서는 UserService 인터페이스와 UserServiceImpl 클래스만 사용한다.
UserService
package ssu.cromi.teamit.service;
import ssu.cromi.teamit.DTO.auth.SignupRequest;
import ssu.cromi.teamit.domain.User;
public interface UserService {
/**
* 회원가입 처리
*/
User register(SignupRequest DTO);
/**
* 아이디 중복 여부 확인
* @return true면 이미 사용 중, false면 사용 가능
*/
boolean existsByUid(String uid);
}
- register(SignupRequest DTO)
- 회원가입 요청을 받아 필요한 검증(아이디·이메일 중복, 비밀번호 암호화 등)을 수행하고, User 엔티티를 생성·저장한 뒤 리턴
- existsByUid(String uid)
- 주어진 uid가 이미 저장되어 있는지 확인하는 메서드
UserServiceImpl
package ssu.cromi.teamit.service.impl;
import jakarta.transaction.Transactional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import ssu.cromi.teamit.DTO.auth.SignupRequest;
import ssu.cromi.teamit.domain.User;
import ssu.cromi.teamit.exceptions.DuplicateEmailException;
import ssu.cromi.teamit.exceptions.DuplicateUsernameException;
import ssu.cromi.teamit.repository.UserRepository;
import ssu.cromi.teamit.service.UserService;
import java.time.LocalDateTime;
import java.util.Set;
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
@Transactional
public User register(SignupRequest dto){
if(userRepository.existsByUid(dto.getUid())){
throw new DuplicateUsernameException("이미 사용중인 아이디입니다.");
}
if(userRepository.existsByEmail(dto.getEmail())){
throw new DuplicateEmailException("이미 사용중인 이메일입니다.");
}
User user = User.builder()
.uid(dto.getUid())
.email(dto.getEmail())
.emailVerified(dto.isEmailVerified())
.nickName(dto.getNickName())
.password(passwordEncoder.encode(dto.getPassword()))
.birthday(dto.getBirthDay())
.roles(Set.of(User.RoleEnum.ROLE_USER).toString())
.point(0L)
.build();
LocalDateTime now = LocalDateTime.now();
user.setCreatedAt(now);
user.setUpdatedAt(now);
return userRepository.save(user);
}
@Transactional
@Override
public boolean existsByUid(String uid){
return userRepository.existsByUid(uid);
}
}
- @Service
- 스프링이 이 클래스를 서비스 계층 빈(bean)으로 자동 등록해서, 다른 곳에서 의존성 주입(Dependency Injection)이 가능하도록 하는 코드
- UserRepository
- JPA를 통해 User 엔티티를 데이터베이스에 저장·조회하는 역할.
- PasswordEncoder
- 비밀번호를 해싱(암호화)할 때 사용.
- 해당 코드는 Security 부분에서 따로 다루려고 한다.
위 두 요소들은 생성자를 통한 생성자 주입 방식으로 두 의존성을 주입받는다.
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
UserServiceImpl 클래스에서는 register와 existsByUid 두가지 메서드가 있다.
위 두 메서드는 @Transactional 어노테이션이 선언되어있다.
해당 어노테이션으로 메소드가 하나의 트랜잭션으로 실행되도록 보장한다.
따라서 회원가입 도중 예외 발생 시 변경된 DB 상태가 모두 롤백된다.
register(SignupRequest dto)메서드를 살펴보면
uid, email 중복 검사를 진행하고, User엔티티를 생성하는 과정을 거친다.
이후 생성 및 수정 시간을 설정하고 이 모든 과정이 정상적으로 완료되면 DB에 저장 후 메서드를 반환한다.
existsByUid(String uid)메서드는 단순하게 UserRepository의 중복 검사 메서드를 호출하여 결과를 반환한다.
프론트엔드에서 실시간 아이디 중복 체크용 API로 활용할 수 있을 것이라고 생각한다.
Controller 패키지
컨트롤러는 많은 기능을 담당한다.
컨트롤러가 하는 역할을 나열해보면,
- HTTP 요청 매핑
- 클라이언트의 HTTP 요청(URI, 메서드)을 적절한 메소드에 연결
- 입력 바인딩 및 검증
- 요청 바디(JSON, form 데이터 등)를 DTO 객체로 바인딩
- 검증 애노테이션으로 유효성 검사 수행
- 서비스 계층 호출
- 어떤 서비스 메서드를 호출할지 결정하고 결과 받아오기
- 응답 생성 및 반환
- 서비스 호출 결과(도메인 엔티티, DTO)를 HTTP 응답 형식으로 변환해서 반환
- 예외 처리 연결
- 컨트롤러 레벨에서 발생한 예외를 @ControllerAdvice + @ExceptionHandler로 묶어 전역 처리하거나, 메소드별로 @ExceptionHandler를 지정
AuthController
회원가입 기능에서는 AuthController만 사용한다. 나중에 로그인 기능 구현에서도 내용을 좀 더 붙여서 사용하지만 일단 회원가입에서 사용하는 부분만 보자.
package ssu.cromi.teamit.controller;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import ssu.cromi.teamit.DTO.auth.JwtResponse;
import ssu.cromi.teamit.DTO.auth.LoginRequest;
import ssu.cromi.teamit.DTO.auth.SignupRequest;
import ssu.cromi.teamit.DTO.common.ApiResponse;
import ssu.cromi.teamit.DTO.common.UserResponse;
import ssu.cromi.teamit.domain.User;
import ssu.cromi.teamit.service.AuthService;
import ssu.cromi.teamit.service.UserService;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/v1/auth")
public class AuthController {
private final UserService userService;
private final AuthService authService;
public AuthController(UserService userService, AuthService authservice){
this.userService = userService;
this.authService = authservice;
}
//회원가입
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<UserResponse> signup(@RequestBody @Valid SignupRequest dto){
User newUser = userService.register(dto);
String createdAtStr = newUser.getCreatedAt()
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ISO_INSTANT);
UserResponse data = new UserResponse(
newUser.getUid(),
newUser.getEmail(),
newUser.getNickName(),
createdAtStr,
newUser.getBirthday()
);
return ApiResponse.success(data, "201", "success");
}
}
- @RestController
- 이 클래스의 모든 메소드가 @ResponseBody가 자동 적용되어, 리턴값이 JSON으로 직렬화되어 클라이언트에 전송됨
- @RequestMapping("/v1/auth")
- 이 컨트롤러 내의 모든 엔드포인트 URI는 /v1/auth를 앞에 붙인다는 의미
- UserService와 AuthService의존성 주입
- AuthService는 로그인, 토큰 발급 등에 사용되는 것이지만 미리 준비해놓음.
- @PostMapping("/users")
- HTTP POST 요청을 /v1/auth/users로 받을 때 실행
- @ResponseStatus(HttpStatus.CREATED)
- 로직이 성공했을 때 응답 HTTP 상태 코드를 201 Created로 지정
- signup() 메서드
- 요청 바디(JSON)를 SignupRequest DTO로 바인딩하고 자동으로 유효성 검사를 수행
- 응답 포맷을 가지고 있는 ApiResponse 객체로 반환
- 내부 처리 과정
- userService.register(dto) 에서 아이디, 이메일 중복 검사, 비밀번호 암호화, DB 저장 등을 수행하고 User엔티티 반환
- createdAt(LocalDateTime)을 클라이언트가 이해하기 쉬운 ISO 8601 문자열(2025-07-23T07:45:00Z 등)로 변환
- 실제로 클라이언트에 노출할 사용자 정보를 UserResponse 객체에 담음
- ApiResponse의 정적 팩토리 메소드를 통해 표준화된 JSON 구조로 감싸서 전송
config 패키지
이 패키지는 애플리케이션 전반의 설정을 모아두는 패키지이다.
이 중 SecurityConfig의 회원가입 부분에서 사용되는 부분 + PasswordConfig 클래스를 한번 살펴보도록 하자.
SecurityConfig
package ssu.cromi.teamit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import ssu.cromi.teamit.security.AuthEntryPointJwt;
import ssu.cromi.teamit.security.AuthTokenFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthEntryPointJwt authEntryPointJwt;
private final AuthTokenFilter jwtAuthTokenFilter;
public SecurityConfig(
AuthEntryPointJwt authEntryPointJwt,
AuthTokenFilter jwtAuthTokenFilter
) {
this.authEntryPointJwt = authEntryPointJwt;
this.jwtAuthTokenFilter = jwtAuthTokenFilter;
}
@Bean
@Order(0)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authEntryPointJwt)
)
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
// JWT 필터 등록 (UsernamePasswordAuthenticationFilter 이전)
.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception{
return authConfig.getAuthenticationManager();
}
}
위 코드는 로그인 과정에서 사용하는 JWT 필터, HTTP 세션 등에 대한 부분이 포함되어 있다.
이 중 회원가입에 영향을 주는 코드는
.authorizeHttpRequests(auth -> auth
.requestMatchers("/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
이 부분이다.
.requestMatchers("/v1/auth/").permitAll() 이 부분은 /v1/auth/ 경로로 들어오는 모든 요청을 인증 없이 허용한다는 의미를 가진다.
PasswordConfig
package ssu.cromi.teamit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
위 코드는 애플리케이션에서 비밀번호 암호화에 사용할 PasswordEncoder를 정의하는 클래스이다.
- @Configuration
- 이 클래스가 스프링 설정 파일임을 나타냄
- @Bean
- 해당 메소드가 반환하는 객체를 스프링 컨테이너에 빈으로 등록
- PasswordEncoder 인터페이스
- 스프링 시큐리티에서 제공하는 비밀번호 암호화 추상화 인터페이스
- encode(rawPassword) : 암호화된 문자열 반환
- matches(rawPassword, encodedPassword) : 평문과 암호화된 비밀번호 비교
- BCryptPasswordEncoder
- BCrypt 해시 알고리즘을 사용하여 단방향 암호화를 수행
- 솔트(salt)를 내부에서 자동 생성하고, 반복 라운드를 적용해 안전성을 높여 줌
- 애플리케이션 어디에서든 @Autowired PasswordEncoder passwordEncoder 또는 생성자 주입으로 BCryptPasswordEncoder 빈을 주입받아 사용할 수 있다.
- passwordEncoder.encode(dto.getPassword()) 이런식으로 비밀번호를 암호화하고 DB에 저장한다.
'IT 프로젝트 공모전(백엔드)' 카테고리의 다른 글
| [Spring boot]Spring Security와 JWT를 활용한 로그인 인증 시스템 구현 (2) | 2025.08.15 |
|---|---|
| [Spring boot] 회원가입 기능구현 3(Repository, DTO) (1) | 2025.07.23 |
| [Spring boot] 회원가입 기능구현 2(Domain) (1) | 2025.07.15 |
| [Spring boot] 회원가입 기능구현 1 (설계, 계층, ERD, SQL 스키마) (0) | 2025.07.15 |
| IT 프로젝트 기획 시작 (1) | 2025.06.20 |