React Spring Boot 연동 로그인_4. 백엔드 : UserService 메소드 작성

송지윤·2026년 1월 4일

React

목록 보기
15/15

UserService 구현

import org.example.backend.domain.user.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 자체 로그인 회원 가입 (존재 여부)

    // 자체 로그인 회원 가입

    // 자체 로그인

    // 자체 로그인 회원 정보 수정

    // 자체/소셜 로그인 회원 탈퇴

    // 소셜 로그인 (매 로그인시 : 신규 = 가입, 기존 = 업데이트)

    // 자체/소셜 유저 정보 조회

}

UserRepository 역할
순수 DB 접근 전담

  • 저장
  • 조회
  • 삭제
  • 존재 여부 확인

하면 안 되는 것

  • 회원 가입 규칙
  • 로그인 정책
  • 비즈니스 판단

DB와 직접 통신하는 건 UserRepository Service 단에서는 필요할 때만 호출해서 사용

생성자 주입의 의미
1. 의존성 주입(DI)
2. 테스트 용이
3. 불변성 보장(final)
Spring이 자동으로 넣어줌

자체 로그인 : 회원 가입시 유저 존재 여부

회원 가입 시 이미 username이 존재하는지 "중복 검증"을 진행

UserRepository

Boolean existsByUsername(String username);

메서드 추가
Spring Data JPA가 메서드 이름 분석, 엔티티 필드 확인, 쿼리 생성, 프록시 구현체 생성

JPA 가 실제로 만드는 SQL

SELECT
    CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END
FROM user_user_entity
WHERE username = ?;

findByUsername(String username) 이렇게 하면 전체 row 조회 불필요한 컬럼까지 로딩됨
existsByUsername(String username) 이렇게 하면 존재 여부만 체크, 성능 훨씬 좋음

UserService

	// 자체 로그인 회원 가입 (존재 여부)
	@Transactional(readOnly = true)
	public Boolean existUser(UserRequestDTO dto) {
		return userRepository.existsByUsername(dto.getUsername());
	}

DB에 조회 쿼리 1번 날리고 그 결과로 true 또는 false 그대로 호출한 쪽에 반환
@Transactional(readOnly = true)
이 트랜잭션은 읽기 전용이다 라고 선언한 것

readOnly = true

  1. 변경 감지(Dirty Checking) 비활성화
    JPA는 기본적으로 엔티티 값이 바뀌면 UPDATE 날릴 준비를 함
    하지만 readOnly = true 작성하면 변경 감지하지 않음.
    성능 향상, 실수로 수정해도 UPDATE 안 날아감

  2. Flush 생략 (Hibernate 기준)
    트랜잭션 종료 시 기본은 flush()
    readOnly -> flush 스킵 불필요한 비용 제거

UserRequestDTO

DTO는 데이터 전달 전용 객체

package com.example.backend.domain.user.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserRequestDTO {
	private String username;
    private String password;
    private String nickname;
    private String email;
}

사용자가 입력한 값을 그대로 담을 통 == DTO
데이터베이스와 직접 통신 == Entity

SecurityConfig

자체 회원 가입 메소드를 작성하기 전 비밀번호를 암호화 시키기 위한 PasswordEncoder를 Bean으로 등록하여 다른 곳에서 주입 받아 사용할 수 있도록 작성

package com.example.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
	// 비밀번호 단방향(BCrypt) 암호화용 Bean
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

PasswordEncoder 타입이 필요하면 BCryptPasswordEncoder 인스턴스를 하나 만들어서 Spring이 관리

자체 로그인 : 회원가입

UserService

passwordEncoder 사용할 수 있게 코드 추가 및 회원 가입할 때 entity에 암호화된 비밀번호를 넘겨줄 거임

package com.example.backend.domain.user.service;

import java.nio.file.AccessDeniedException;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.backend.domain.user.dto.UserRequestDTO;
import com.example.backend.domain.user.entity.UserEntity;
import com.example.backend.domain.user.entity.UserRoleType;
import com.example.backend.domain.user.repository.UserRepository;


@Service
public class UserService {
	private final UserRepository userRepository;
	private final PasswordEncoder passwordEncoder;
	
	public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
		this.userRepository = userRepository;
		this.passwordEncoder = passwordEncoder;
	}
	
	// 자체 로그인 회원 가입 (존재 여부)
	@Transactional(readOnly = true)
	public Boolean existUser(UserRequestDTO dto) {
		return userRepository.existsByUsername(dto.getUsername());
	}
	
	// 자체 로그인 회원 가입
	@Transactional
	public Long addUser(UserRequestDTO dto) {
		if(userRepository.existsByUsername(dto.getUsername())) {
			throw new IllegalArgumentException("이미 유저가 존재합니다.");
		}
		
	    UserEntity entity = UserEntity.builder()
	            .username(dto.getUsername())
	            .password(passwordEncoder.encode(dto.getPassword()))
	            .isLock(false)
	            .isSocial(false)
	            .roleType(UserRoleType.USER) // 우선 일반 유저로 가입
	            .nickname(dto.getNickname())
	            .email(dto.getEmail())
	            .build();
	    
	    return userRepository.save(entity).getId();
	}

}

자체 로그인 : 회원 정보 수정

UserRepository

Optional<UserEntity> findByUsernameAndIsLockAndIsSocial(String username, Boolean isLock, Boolean isSocial);

회원 정보 수정시 자체 로그인 여부, 잠김 여부를 확인해야함.
자체로그인과 소셜로그인 검증 안하면 사용자가 강제로 소셜 로그인 데이터를 바꿔버릴 수 있음. 그래서 소셜 로그인인지 아닌지 확인해야함.
반환 타입 Optional
결과 없을 수도 있음, null 직접 처리 x, Optional로 명시적으로 표현

UserService

	// 자체 로그인 회원 정보 수정
	public Long updateUser(UserRequestDTO dto) throws AccessDeniedException{
	    
		// 본인만 수정 가능 검증
	    String sessionUsername = SecurityContextHolder.getContext().getAuthentication().getName();
	    if (!sessionUsername.equals(dto.getUsername())) {
	        throw new AccessDeniedException("본인 계정만 수정 가능");
	    }
	    
	    // 조회
	    UserEntity entity = userRepository.findByUsernameAndIsLockAndIsSocial(dto.getUsername(), false, false)
	            .orElseThrow(() -> new UsernameNotFoundException(dto.getUsername()));

	    // 회원 정보 수정
	    entity.updateUser(dto);

	    return userRepository.save(entity).getId();
	}

dto로 부터 user 정보 받아 올거고
로그인된 사용자(현재 요청을 보낸 사용자)의 user 정보를 받아옴
세션 / 토큰(JWT) 어디든 상관없이 Spring Security가 인증을 완료하면 그 정보가 SecurityContext에 저장됨

SecurityContextHolder

Spring Security 의 전역 저장소

  • 현재 요청의 보안 정보 보관
  • ThreadLocal 기반

getContext()

현재 요청에 대한 SecurityContext
여기에 들어있는 것

  • Authentication (인증 정보)
  • 권한(Role)

getAuthentication()

누가 로그인했는지에 대한 정보

Authentication auth;

안에 들어있는 것들

  • principal (사용자 정보)
  • authorities (권한)
  • authenticated (인증 여부)

getName()

로그인한 사용자의 식별자
보통 username 또는 userId

인증이 안된 상태면 null 이거나 anonymousUser 일 수 있음

그래서

	    if (!sessionUsername.equals(dto.getUsername())) {
	        throw new AccessDeniedException("본인 계정만 수정 가능");
	    }
	    UserEntity entity = userRepository.findByUsernameAndIsLockAndIsSocial(dto.getUsername(), false, false)
	            .orElseThrow(() -> new UsernameNotFoundException(dto.getUsername()));

username이 같고, 잠기지 않았고, 소셜 ㄱ정이 아닌 유저를 DB에서 조회한다.

SELECT *
FROM user_user_entity
WHERE username = ?
  AND is_lock = false
  AND is_social = false;

orElseThrow()

Optional 안에 값이 있으면 그 값 반환, 없으면 예외 던짐

UsernameNotFoundException

Spring Security 표준 예외
인증/인가 흐름과 잘 맞음
나중에 글로벌 예외 처리하기 쉬움

UserEntity

    public void updateUser(UserRequestDTO dto) {
    	this.email = dto.getEmail();
    	this.nickname = dto.getNickname();
    }

email 과 nickname 정보만 수정할 수 있도록 세팅

0개의 댓글