이제 회원가입과 로그인은 마쳤으니 JWT를 이용하여
Read Update delete 를 만들어보고자 한다.
findById를 이용하여 찾는 것이 아니라
회원가입 하면 주어지는 토큰을 이용하여 그 토큰에 할당된 id를 찾을 것이다.
일단 조회를 위한 id(pk)를 제외한 DTO를 만들었음
(User + UserAdditonalInfo)
@Service
@RequiredArgsConstructor
public class UserProfileService {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final UserAdditionalInfoRepository userAdditionalInfoRepository;
public UserProfileDTO readUserProfile(String token) {
Claims claims = jwtUtil.extractAllClaims(token);
String userId = claims.getSubject();
User user = userRepository.findById(Long.valueOf(userId))
.orElseThrow(() -> new RuntimeException("use r를 찾을 수 없습니다"));
UserAdditionalInfo additionalInfo = userAdditionalInfoRepository.findByUserId(Long.valueOf(userId))
.orElseThrow(() -> new RuntimeException("user 추가정보를 찾을 수가 없습니다."));
UserProfileDTO userProfile = new UserProfileDTO(
user.getUserId(),
user.getEmail(),
user.getName(),
user.getPhone(),
additionalInfo.getProfileImage(),
additionalInfo.getNickname(),
additionalInfo.getGender(),
user.getRole()
);
return userProfile;
}
}
authService에서는 인증을 담당하기로 하고
userProfileService를 만들어 조회 수정 삭제를 담당하기로 하였다.
@GetMapping("/profile")
public ResponseEntity<?> readUserProfile(@RequestHeader("Authorization") String token) {
try {
// 토큰에서 bearer 부분 제거
String jwtToken = token.substring(7);
UserProfileDTO userProfileDTO = userProfileService.readUserProfile(jwtToken);
return ResponseEntity.ok(userProfileDTO);
} catch (Exception e) {
return ResponseEntity.status(400).body("프로필 조회 중 오류 발생");
}
}
이번에는 URL 경로에 ID 포함 (/profile?id=1)를 방식을 사용하지 않았다. 지금 이 pk(단순 숫자)가 민감한 정보는 아니지만 개인적으로 URL에 ID를 포함시켜 ID 값이 URL에 노출되는 것이 싫었다.
또한 아이폰 유저들은 웹으로 접속할텐데 웹 주소창이 길어지는 것 또한 싫었다.
내가 사용한 방식은 Authorization 헤더에 Bearer 형식으로 포함시키는 것이다. 엑세스 토큰을 사용하여 인증된 상태에서 자원에 접근하기로 하였다!
Bearer형식이란?
Bearer: Bearer는 HTTP Authorization 헤더에서 액세스 토큰을 전달할 때 사용하는 인증 방식이다.
Bearer는 "Bearer token"의 약자로, 토큰이 인증을 위한 "Bearer" 즉, 권한을 부여받은 자격 증명이라는 의미를 담고 있다.
형식: Authorization 헤더에 Bearer<token>형식으로 액세스 토큰을 포함시킨다. 이때<token>부분에는 실제 액세스 토큰 값이 들어간다!
단점이라고 하면 캐싱 문제이다.
HTTP 요청 본문은 캐싱되지 않는다. 이는 요청이 자주 반복되는 경우 성능에 영향을 줄 수 있다고 한다. 하지만 뭐 조회 수정 삭제를 반복적으로 요청할 일은 많지 않다고 생각하였다!

여튼 조회에는 성공하였다!!
수정도 마찬가지로 토큰을 이용해서 인증을 받고 수정할 것이다!
수정은 민감한 정보(비밀번호) 를 제외하면 토큰으로 인증하고
비밀번호는 암호화가 되어 있기에 이메일 인증을 통하여 임시비밀번호를 발급하고 덮어씌우는 방식으로 진행하겠다.

정보 수정용 DTO 생성
public UserProfileDTO updateProfile(String token, UserProfileUpdateDTO userUpdateDto) {
Claims claims = jwtUtil.extractAllClaims(token);
String userId = claims.getSubject();
User user = userRepository.findById(Long.valueOf(userId))
.orElseThrow(() -> new RuntimeException("user를 찾을 수 없습니다"));
UserAdditionalInfo additionalInfo = userAdditionalInfoRepository.findByUserId(Long.valueOf(userId))
.orElseThrow(() -> new RuntimeException("user 추가정보를 찾을 수가 없습니다."));
if (userUpdateDto.getEmail() != null) {
user.setEmail(userUpdateDto.getEmail());
}
if (userUpdateDto.getPhone() != null) {
user.setPhone(userUpdateDto.getPhone());
}
if (userUpdateDto.getNickname() != null) {
additionalInfo.setNickname(userUpdateDto.getNickname());
}
userRepository.save(user);
userAdditionalInfoRepository.save(additionalInfo);
// 일단 전체 내용 반환 (프론트와 상의 후 추후 수정)
return new UserProfileDTO(
user.getUserId(),
user.getEmail(),
user.getName(),
user.getPhone(),
additionalInfo.getProfileImage(),
additionalInfo.getNickname(),
additionalInfo.getGender(),
user.getRole()
);
}
jwt토큰을 이용해 인증을 하여 이메일, 전화번호, 닉네임등을 수정 후에 변화된것을 확인하기 위해 일단은 전체내용 반환하였음

토큰을 집어넣고 테스트하니 정상적으로 변화 완료!
라고 생각했었는데.... 뭔가 클래스들이 조금 꼬인 거 같다... 정리를 한번 해 줄 필요가 있을 것 같다.
현재 userAdditionalService에서는 추가적인 정보 저장 + 사진 로직을 가지고 있다. (벌써 단일책임 위반)
또한 userProfileService에서는 프로필에 대한 RUD를 만들고 있는데 (이름과 관련이 없기도 하고, 조회 수정만 있고 저장은 없는 것도 뭔가 이상하였다.)
더 늦기전에 리펙토링을 하고 가보려고 한다...!
일단 사진에 관련된 클래스 및 메소드는 따로 만드는 것이 좋을 것 같다는 생각이 들었다!
사진에 대한 클래스 (사진 저장, 업데이트 삭제) 를 담당하는 클래스
추가,개인 정보에 대한 클래스 (저장) <
(개인 및 추가 정보가 합쳐져서 프로필이 만들어지니 따로 분리하고, RUD는 전체적인 프로필 로직에서 함께 관리한다.)
비밀번호 재발급 및 이메일 인증 클래스
(이메일 인증 및 복잡한 로직이 예상되니 따로 뺴기로)
ProfileService -> 전반적인 프로필에 관한 RUD를 담당
(Create는 추가정보, 개인정보로 이미 저장을 했으니 여기선 제외)
개인정보와 추가 정보를 분리하였으면 암호화? 보안을 강화해야 할 것 같음.
일단은 일정상 개발을 진행하고 추후에 리펙토링으로 묶어야 할 것 같다!
public UserAdditionalInfo updateImage(String token, MultipartFile profileImageFile){
try {
Claims claims = jwtUtil.extractAllClaims(token);
String userId = claims.getSubject();
UserAdditionalInfo additionalInfo = userAdditionalInfoRepository.findByUserId(Long.valueOf(userId))
.orElseThrow(() -> new RuntimeException("user 추가정보를 찾을 수가 없습니다."));
if (profileImageFile != null && !profileImageFile.isEmpty()) {
String imageFile = saveImage(profileImageFile);
additionalInfo.setProfileImage(imageFile);
}
return userAdditionalInfoRepository.save(additionalInfo);
} catch (RuntimeException e) {
throw new RuntimeException(e);
}
}
@PostMapping("/updateProfileImage")
public ResponseEntity<?> updateProfileImage(@RequestPart("profileImage") MultipartFile profileImageFile, @RequestHeader("Authorization") String token) {
try {
String jwtToken = token.substring(7);
UserAdditionalInfo additionalInfo = userAdditionalInfoService.updateImage(jwtToken, profileImageFile);
return ResponseEntity.ok(additionalInfo);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.badRequest().body("프로필 이미지 업데이트 중 오류 발생");
}
}
이미지 수정까지 완료되었다!
(중복되는 코드는 추후에 빠르게...? 리펙토링 해보도록 하겠다)

application properties에 설정 정보 추가
@RestController
@RequestMapping("/email")
public class EmailController {
@Autowired
private JavaMailSender mailSender;
@GetMapping("/send-test")
public String sendTestEmail() {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(""); // 상대방 이메일
message.setSubject("Test Email");
message.setText("메일 잘 가나여 ");
mailSender.send(message);
return "Email sent successfully!";
} catch (Exception e) {
return "Error sending email: " + e.getMessage();
}
}
}
이메일 컨트롤러 하나 만들고 일단 메일이 가는지 확인

오예~ 잘 오네용
Optional findByEmail(String email);
UserRepository 에 이메일 찾기 추가!
@Service
public class EmailService {
private final JavaMailSender mailSender;
@Autowired
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
}
}
이메일 서비스 로직 하나 생성했다.
이렇게 비밀번호를 재발급하고 메일로 전송한다.
전 비밀번호인 1234를 입력하자
java.lang.RuntimeException: 비밀번호가 일치 하지 않습니다.
라고 뜬다!


발급받은 비밀번호로 새롭게 로그인을 성공하였다!
그런데...! 재발급 로직을 거친 이후에만 비밀번호를 변경을 해야한다는 생각을 뒤늦게... 하였고...... 어떻게 할지 고민하다가
토큰을 하나 추가해서 인증상태를 점검하기로 하였음.
CREATE TABLE Token (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
token VARCHAR(255) NOT NULL UNIQUE,
expiry_date DATETIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
);
Token 데이터 베이스 생성
@Entity
@Getter
@Setter
public class PasswordToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
private String token;
private LocalDateTime expiryDate;
public PasswordToken() {
}
public PasswordToken( User user, String token, LocalDateTime expiryDate) {
this.user = user;
this.token = token;
this.expiryDate = expiryDate;
}
}
public interface PasswordTokenRepository extends JpaRepository<PasswordToken, Long> {
Optional<PasswordToken> findByToken(String token);
}
PasswordTokenRepository, PasswordToken
을 생성하였음
@Service
@RequiredArgsConstructor
public class PasswordResetService {
private final EmailService emailService;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final PasswordTokenRepository tokenRepository;
public void generateTempPassword(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("이메일 인증을 하는데 이런 이메일을 가진 유저가 없어요"));
String tempPassword = UUID.randomUUID().toString().substring(0, 8);
user.setPassword(passwordEncoder.encode(tempPassword));
userRepository.save(user);
String token = UUID.randomUUID().toString();
PasswordToken passwordToken = new PasswordToken(user, token, LocalDateTime.now().plusHours(1));
tokenRepository.save(passwordToken);
String subject = "임시 비밀번호를 발급해드렸습니다.";
String text = "고객님의 임시 비밀번호는 " + tempPassword + "입니다. 정확하게 입력해주세요!\n" +
"비밀번호 변경을 위해 다음 링크를 사용하세요: " +
"localhost:8080/change-password?token=" + token;
emailService.sendEmail(user.getEmail(), subject, text);
}
}
서비스 로직에 토근발급 로직을 추가하였음(1시간으로)
후에 메일에 링크를 첨부하여 보다 쉽게 변경 가능하도록 함
@RestController
@RequiredArgsConstructor
public class EmailController {
private final PasswordResetService passwordResetService;
private final UserRepository userRepository;
private final PasswordTokenRepository tokenRepository; // 새로 추가
private final PasswordEncoder passwordEncoder;
@PostMapping("/request-temp-password")
public ResponseEntity<?> requestTempPassword(@RequestBody UserProfileUpdateDTO updateDTO) {
try {
String email = updateDTO.getEmail();
passwordResetService.generateTempPassword(email);
return ResponseEntity.ok("임시 비밀번호를 이메일로 발송했습니다.");
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("임시 비밀번호 발급 실패");
}
}
@PostMapping("change-password")
public ResponseEntity<?> TempPassword(@RequestParam("token") String token, @RequestBody UserProfileUpdateDTO profileDTO) {
try {
PasswordToken passwordToken = tokenRepository.findByToken(token)
.orElseThrow(() -> new RuntimeException("유효하지 않은 토큰임"));
if (passwordToken.getExpiryDate().isBefore(LocalDateTime.now())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("토큰이 만료되었음");
}
User user = passwordToken.getUser();
String newPassword = profileDTO.getPassword();
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
tokenRepository.delete(passwordToken);
return ResponseEntity.ok("비밀번호 변경 ok");
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("비밀번호 변경 실패!");
}
}
}
이름이 emaiController인것은 아직 수정중이라서 그렇다!


비밀번호 변경까지 완료~~
다음 시간에는 일단 클래스부터 정리를 조금 하고 진행하겠다!