웹개발/SpringBoot

보안과 권한 시스템 - 4. Service/ServiceImpl

bergerac 2025. 6. 5. 09:11
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);
}

 

 

회원 정보 수정

  1. 사용자 존재 여부 확인
  2. 새 비밀번호가 비어있지 않다면 → 확인용 비밀번호와 일치하는지 검사
  3. 일치하면 암호화하여 저장
  4. updatedAt 필드 갱신
  5. 저장 후 응답 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);
}