16:00 ~ 17:00 동시성 제어 ( 스탠다드반 세션 )
오늘은 심화 프로젝트를 진행했다.
Dto와 entity, 기본적인 설정은 다 있다고 가정하고 포스팅을 하겠다.
repository는 생략하도록 하겠다.
1️⃣ 회원 탈퇴, 회원 생성(v2) 기능
Controller
// 회원 탈퇴
@DeleteMapping
public ResponseEntity<Void> delete(
@LoginUser Long userId,
@RequestBody DeleteUserRequestDto requestDto
) {
userService.delete(userId, requestDto.getPassword());
return new ResponseEntity<>(HttpStatus.OK);
}
회원탈퇴 기능으로 DeleteMapping으로 하고 매개변수로는 로그인 기능을 구현했기 때문에 userId는 커스텀 어노테이션인 @LoginUser를 이용한다. requestDto에는 비밀번호를 받고 서비스에서 비즈니스 로직으로 요청을 보낸다.
Service
// 회원 탈퇴
@Transactional
@Override
public void delete(Long userId, String password) {
// 삭제할 유저를 조회
User findUser = userRepository.findByIdOrElseThrow(userId);
// 1. 이미 탈퇴한 회원일 경우
if(findUser.isDeleted()) {
throw new RuntimeException("이미 탈퇴한 사용자 입니다.");
}
// 2. 비밀번호 일치 여부 검증
if(!passwordEncoder.matches(password, findUser.getPassword())){
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
}
// 탈퇴 처리 여부
findUser.deleteUser();
}
서비스 쪽에서는 삭제할 유저를 DB에서 조회하여 가져와야 하므로, 먼저 찾는 작업을하고, isDeleted는 이번 회원탈퇴 기능을 구현하면서 이 필드가 필요하겠다하면서 Boolean 타입으로 필요할것 같았다. 이 필드는 삭제 여부가 true면 예외 처리를 발생시키는 형식으로 구현하였으며, 아니라면 바로 비밀번호 일치여부 검증으로 나아간다. 그러고 둘다 검증에 걸리지 않게 된다면 해당 유저는 탈퇴처리여부가 false에서 true가 되며 다시는 회원가입 쪽에서 회원가입과 로그인 할 수 없다.
@Transactional
@Override
public UserResponseDto Signup(String email, String password, String name, String address, UserRole role) {
// 기존 유저 존재 여부 확인
Optional<User> findUserEmail = userRepository.findByEmail(email);
if(findUserEmail.isPresent()) {
User existUser = findUserEmail.get(); // Optional get() 사용하면 객체에 접근가능함.
// 이미 탈퇴한 사용자 이면 재가입 불가능
if(existUser.isDeleted()) {
throw new RuntimeException("이미 탈퇴한 회원입니다. 재가입이 불가능합니다.");
}
throw new RuntimeException("이미 가입된 회원입니다.");
}
// 비밀번호 암호화
String encodePassword = passwordEncoder.encode(password);
User addUser = new User(email, encodePassword, name, address, role);
User savedUser = userRepository.save(addUser);
return new UserResponseDto(
savedUser.getUserId(),
savedUser.getEmail(),
savedUser.getName(),
savedUser.getAddress(),
savedUser.getRole(),
savedUser.getCreatedAt(),
savedUser.getUpdatedAt()
);
}
좀더 보완된 회원가입 생성 비즈니스 로직이다.
2️⃣ 로그인 기능
로그인 기능은 Spring security 없이 JWT 기반으로 개발하였다.
JwtProvier
jwt을 생성하고 검증하는 역할
/**
* JWT(Token)을 생성하고 검증하는 역할인 클래스
*/
@Component
public class JwtProvider {
private Key key;
private final long EXPIRATION_TIME = 1000 * 60 * 60; // 토큰 유효시간 : 1시간
@PostConstruct
public void init() {
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
}
public String createToken(Long userId) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key)
.compact();
}
public Long getUserIdFromToken(String token) {
String subject = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
return Long.parseLong(subject);
}
}
LoginUserArgumentResolver
파라미터 형식으로 쓸것 이기 때문에 supportParameter 메소드 내에서 hasParameterAnnotation으로 LoginUser 커스텀 어노테이션을 등록해주었다.
jwt를 제공하는 클래스로 Header에 key값으로는 Authorization, 값에는 Bearer { 토큰 값 } 형식으로 요청 해야한다. ( @LoginUser )
@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtProvider jwtProvider;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginUser.class) && parameter.getParameterType().equals(Long.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
String header = webRequest.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new RuntimeException("JWT 토큰이 필요합니다.");
}
String token = header.substring(7);
return jwtProvider.getUserIdFromToken(token);
}
}
LoginUser
커스텀 어노테이션 LoginUser
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser { }
Controller
컨트롤러는 /authors/login 로그인을 하기위한 메소드만 만들어 주었으며, 추후에 로그아웃과 로그인필터까지 고민해보아야 겠다. ( 당연히 회원가입이 된 유저만 로그인이 가능함 )
JSON형태로 받기위해 LoginResponseDto로 반환 받는다. 처음에는 <?> 이렇게 해서 String 형태로 토큰값이 그대로 반환 받아서 지저분했던점 을 해결했다.
@RestController
@RequestMapping("/authors")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
// 로그인
@PostMapping("/login")
public ResponseEntity<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto requestDto) {
String token = authService.login(requestDto);
return new ResponseEntity<>(new LoginResponseDto(token),HttpStatus.OK);
}
}
Service
findByEmailOrElseThrow 메소드는 repository 내에서 선언한 디폴트 메소드이며 이메일이 없다면 예외처리까지 해주는 메소드이다. 우선 찾을 유저의 이메일을 찾고, 탈퇴한 회원이면 로그인이 불가능하게 하며, 비밀번호 일치 검증 여부를 조건을 걸어주며, 둘다 통과되면, 토큰 값을 반환한다.
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService{
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
@Override
public String login(LoginRequestDto requestDto) {
User user = userRepository.findByEmailOrElseThrow(requestDto.getEmail());
// 탈퇴한 회원이면 로그인 불가능
if(user.isDeleted()) {
throw new RuntimeException("탈퇴한 회원입니다. 로그인이 불가능합니다.");
}
// 비밀번호 일치 여부 검증
if(!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
}
return jwtProvider.createToken(user.getUserId());
}
}
TMI
요즘 눈이 안좋은것 같다. 전자기기를 너무 오래 봤더니 내일은 무조건 외출해 나가서 진료를 받아야 겠다.
(한쪽 눈만 안좋은 경우는 살면서 처음이다)
'[내일배움캠프-Sparta] > Spring 6기' 카테고리의 다른 글
| TIL 46 [ Spring 심화 프로젝트 ( Day 5 ) - 마무리 작업 ( ppt, 대본, ERD/API 명세서 리뉴얼) ] (0) | 2025.04.28 |
|---|---|
| TIL 45 [ Spring 심화 프로젝트 ( Day 4 ) - 구현 ( JWT 로그인 필터 기능 ), PR 리뷰/병합 ] (0) | 2025.04.25 |
| TIL 43 [ Spring 심화 프로젝트 ( Day2 ) - 구현 ( 회원 가입, 전체 유저 조회, 회원 정보 수정, 암호화 처리 ) ] (0) | 2025.04.23 |
| TIL 42 - [ Spring 심화 프로젝트 ( Day 1 ) - 설계 ( 와이어프레임, ERD, API 명세서 ) ] (0) | 2025.04.22 |
| 테스트 코드 ( 이론 ) (0) | 2025.04.21 |