com.example.korea_sleepTech_springboot
├── controller
│ └── UserController.java
├── service
│ ├── UserService.java // 인터페이스
│ └── UserServiceImpl.java // 실제 구현체
├── repository
│ └── UserRepository.java
├── dto
│ ├── user/
│ │ ├── request/UserUpdateRequestDto.java
│ │ └── response/GetUserResponseDto.java
├── entity
│ └── User.java
└── common
├── constants/ResponseMessage.java
└── dto/ResponseDto.java
Service 계층이란?
비즈니스 로직을 담당하는 계층이며 사용자의 의도에 따라 데이터베이스를 읽고, 쓰고, 외부시스템과 연동하고, 규칙을 검증하며 애플리케이션의 동작을 결정하는 모든 코어로직이다.
🔹 트랜잭션 관리
- Spring에서는 @Transactional 애너테이션으로 트랜잭션 경계를 지정할 수 있음
- 현재 예시는 단일 DB 작업이지만, 여러 작업이 포함될 경우 꼭 명시해야 함
🔹 비즈니스 로직 분리
- Controller는 흐름을 조율
- Service는 그 흐름을 어떻게 처리할지 결정
🔹 응답 객체 구조
- ResponseDto<T>로 일관된 응답 포맷 구성
- 메시지, 데이터, 상태 등을 한 번에 담아 클라이언트로 반환
🔹 서비스 계층 테스트 용이성
- 인터페이스 기반 설계 덕분에 테스트에서 @MockBean으로 가짜 주입 가능
- 단위 테스트 및 통합 테스트 작성이 수월
UserService
1. 인터페이스 정의 : UserService
인터페이스 분리의 장점
- 관심사 분리: 구현체와 분리되어 있어 테스트나 확장에 용이
- Mocking 가능: 테스트 코드에서 구현체 없이도 서비스 계층 테스트 가능
- 구현체 교체 가능성 확보: UserServiceImpl 외에 FakeUserService, CachedUserService 등으로 교체해도 영향 없음
public interface UserService {
ResponseDto<GetUserResponseDto> getUserInfo(String userEmail);
ResponseDto<GetUserResponseDto> updateUserInfo(String userEmail, @Valid UserUpdateRequestDto dto);
ResponseDto<Void> deleteUser(String userEmail);
}
2. 실제구현 : UserServiceImpl
- @Service: 스프링이 이 클래스가 서비스 역할임을 인식하고 빈으로 등록
- @RequiredArgsConstructor: final 필드 기반으로 생성자 자동 주입
- BCryptPasswordEncoder: 비밀번호 암호화에 사용됨
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
회원 정보 조회
- Repository에서 이메일 기반으로 User 검색
- null이면 실패 응답 반환
- 존재하면 DTO로 변환 후 성공 응답 반환
✅ 여기서 포인트
- 컨트롤러에서는 예외를 try-catch로 잡지 않음 → 서비스에서 판단 후 ResponseDto로 감쌈
- 예외 대신 ResponseDto.setFailed() 방식도 가능, 혹은 orElseThrow 방식도 병행 사용 가능
@Override
public ResponseDto<GetUserResponseDto> getUserInfo(String userEmail) {
User user = userRepository.findByEmail(userEmail).orElse(null);
if (user == null) {
return ResponseDto.setFailed(ResponseMessage.NOT_EXISTS_USER);
}
GetUserResponseDto data = new GetUserResponseDto(user);
return ResponseDto.setSuccess(ResponseMessage.SUCCESS, data);
}
회원 정보 수정
- 사용자 존재 여부 확인
- 새 비밀번호가 비어있지 않다면 → 확인용 비밀번호와 일치하는지 검사
- 일치하면 암호화하여 저장
- updatedAt 필드 갱신
- 저장 후 응답 DTO 반환
Point
- 도메인 유효성 검사는 서비스에서 처리해야 한다
- 컨트롤러는 단순 전달자일 뿐
- 예외는 명확한 메시지를 가진 커스텀 예외로 대체 가능 (ex. UserNotFoundException)
- 비밀번호 암호화는 서비스 단에서 명확하게 책임지는 게 좋음
@Override
public ResponseDto<GetUserResponseDto> updateUserInfo(String userEmail, UserUpdateRequestDto dto) {
User user = userRepository.findByEmail(userEmail)
.orElseThrow(() -> new EntityNotFoundException(ResponseMessage.NOT_EXISTS_USER));
if (dto.getPassword() != null && !dto.getPassword().isBlank()) {
if (!dto.getPassword().equals(dto.getConfirmPassword())) {
throw new IllegalArgumentException(ResponseMessage.NOT_MATCH_PASSWORD);
}
String encodedPassword = bCryptPasswordEncoder.encode(dto.getPassword());
user.setPassword(encodedPassword);
}
user.setUpdatedAt(LocalDateTime.now());
userRepository.save(user);
GetUserResponseDto data = new GetUserResponseDto(user);
return ResponseDto.setSuccess(ResponseMessage.SUCCESS, data);
}
회원탈퇴
- 회원 조회 후 없으면 예외
- 있으면 삭제 → ResponseDto<Void>로 응답
@Override
public ResponseDto<Void> deleteUser(String userEmail) {
User user = userRepository.findByEmail(userEmail)
.orElseThrow(() -> new EntityNotFoundException(ResponseMessage.NOT_EXISTS_USER));
userRepository.delete(user);
return ResponseDto.setSuccess(ResponseMessage.SUCCESS, null);
}
'웹개발 > SpringBoot' 카테고리의 다른 글
| 보안과 권한 시스템의 핵심 - 6. Config/WevSecurityConfig (1) | 2025.06.07 |
|---|---|
| 보안과 권한 시스템 핵심 - 5. filter/JwtAuthenticationFilter (1) | 2025.06.06 |
| 보안과 권한 시스템 - 3. Controller (1) | 2025.06.04 |
| 보안 인증과 권한 시스템 - 2. Repository (2) | 2025.06.03 |
| 보안 인증과 권한 시스템 - 1. Entity (1) | 2025.06.02 |