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이 존재하는지 "중복 검증"을 진행
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) 이렇게 하면 존재 여부만 체크, 성능 훨씬 좋음
// 자체 로그인 회원 가입 (존재 여부)
@Transactional(readOnly = true)
public Boolean existUser(UserRequestDTO dto) {
return userRepository.existsByUsername(dto.getUsername());
}
DB에 조회 쿼리 1번 날리고 그 결과로 true 또는 false 그대로 호출한 쪽에 반환
@Transactional(readOnly = true)
이 트랜잭션은 읽기 전용이다 라고 선언한 것
변경 감지(Dirty Checking) 비활성화
JPA는 기본적으로 엔티티 값이 바뀌면 UPDATE 날릴 준비를 함
하지만 readOnly = true 작성하면 변경 감지하지 않음.
성능 향상, 실수로 수정해도 UPDATE 안 날아감
Flush 생략 (Hibernate 기준)
트랜잭션 종료 시 기본은 flush()
readOnly -> flush 스킵 불필요한 비용 제거
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
자체 회원 가입 메소드를 작성하기 전 비밀번호를 암호화 시키기 위한 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이 관리
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();
}
}
Optional<UserEntity> findByUsernameAndIsLockAndIsSocial(String username, Boolean isLock, Boolean isSocial);
회원 정보 수정시 자체 로그인 여부, 잠김 여부를 확인해야함.
자체로그인과 소셜로그인 검증 안하면 사용자가 강제로 소셜 로그인 데이터를 바꿔버릴 수 있음. 그래서 소셜 로그인인지 아닌지 확인해야함.
반환 타입 Optional
결과 없을 수도 있음, null 직접 처리 x, Optional로 명시적으로 표현
// 자체 로그인 회원 정보 수정
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에 저장됨
Spring Security 의 전역 저장소
현재 요청에 대한 SecurityContext
여기에 들어있는 것
누가 로그인했는지에 대한 정보
Authentication auth;
안에 들어있는 것들
로그인한 사용자의 식별자
보통 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;
Optional 안에 값이 있으면 그 값 반환, 없으면 예외 던짐
Spring Security 표준 예외
인증/인가 흐름과 잘 맞음
나중에 글로벌 예외 처리하기 쉬움
public void updateUser(UserRequestDTO dto) {
this.email = dto.getEmail();
this.nickname = dto.getNickname();
}
email 과 nickname 정보만 수정할 수 있도록 세팅