그라찌에 6 -> 클래스 정리하기

류희수·2024년 8월 20일

산더미 같이 늘어난 나의 클래스...

정신없이 만들다보니.. 단일책임 원칙이 자꾸 깨지게 되기도하고... 지저분하기도 해서 일단 정리를 조금하려고 한다..

저번에 고민하던 그 문제는

멘토님께서

좀 어려우실 수 있지만, user와 user additional info는 서로 물리적 거리가있는 다른 디비에 들어있다고 생각하시면 좋을것같아요!

라는 피드백을 해주셨다!
그래서 물리적인 거리를 두고 생각하여 profile 서비스에 모든 로직을 넣는다면 나눈 의미가 없어지므로 user, userinfo 두 서비스로 분리해서 사용하기로 하였다.
(다만 조회나 업데이트 같은 경우에는 서비스는 분리하고 DTO로 필요한 부분만 뽑아서 사용할 예정이다!)

일단 간단하게 패키지들을 나누어 정리하기 시작하였음!

이제 문제는 profile... 패키지들을 없애야 하는데.......
이래서 첫 단추가 중요한거 같다.....
감이 안와서 한참을 멍 때렸다ㅠ,,,,

  1. 일단 하나의 서비스 로직을 User | additional 로 거리감을 두고 생각하자
  2. 서비스 로직은 분리해서 만들자!
    3. 너무 객체지향적에 집착하지 말고 필요에 따라 생각해서 진행하기!

서비스 로직 분리하기

현재 update 로직에는 DTO를 사용하여
User, UserEntity가 섞여서 들어가 있다. 단일책임원칙과 유지보수를 위하여 따로따로 만들기로 하자!


UserService

일단 User(개인정보) 부터 진행하겠다.

public class UserDTO {
    
    private String password;


    private String email;


    private String name;


    private String phone;
    
     public UserDTO(String email, String name, String phone) {
        this.email = email;
        this.name = name;
        this.phone = phone;
    }

// 이거 추가!! 
    public UserDTO() {
    }

id와 역할을 제외하고 변경할 수 있게 DTO들을 생성
(이래서 휴대폰 인증이 참 좋다는 생각을 하였다,,,)

추가
생성자를 비밀번호를 빼고 3개짜리를 만들어서 기본 생성자가 생성되지 않았음.
Jackson은 기본 생성자가 필요하여 JSON을 객체로 변환할 때 사용한다!.

추가
클래스 하나 아끼다가 똥 될거 같았고
DTO하나에 2개의 역할을 나눠서 하는 것 같았다.
또한 패스워드 인증을 위하여 패스워드가 2개 필요하였기에 password를 빼고 따로 DTO를 분리하였음.
하지만 기록을 위해 저 위에 코드는 수정하지 않았다!

  1. UserJoinService를 없애고 join 메소드를 가져오도록 하였다.
    UserJoinService = 삭제
    -> UserService

readUser와 updateUser, delete등을 가져오는 과정에서

 Claims claims = jwtUtil.extractAllClaims(token);
        String userId = claims.getSubject();

        User user = userRepository.findById(Long.valueOf(userId))
                .orElseThrow(() -> new RuntimeException("user를 찾을 수 없습니다"));

이 토큰을 가져오는 부분이 너무 중복되어 하나의 메소드로 따로 뺴기로 하였음.

@Service
@RequiredArgsConstructor
public class UserService {

   private final UserRepository userRepository;
   private final PasswordEncoder passwordEncoder;
   private final JwtUtil jwtUtil;

   public User joinUser(User user) {
       String password = user.getPassword();
       String encodePassword = passwordEncoder.encode(password);
       user.setPassword(encodePassword);
       user.setRole(Role.CUSTOMER);
       return userRepository.save(user);
   }

   private User getUserToken(String token) {
       Claims claims = jwtUtil.extractAllClaims(token);
       String userId = claims.getSubject();

       return userRepository.findById(Long.valueOf(userId))
               .orElseThrow(() -> new RuntimeException("user를 찾을 수 없습니다"));
   }

   public User updatePassword(String token, PasswordDTO passwordDTO) {
       User user = getUserToken(token);

       if (passwordDTO.getCurrentPassword() == null || passwordDTO.getNewPassword() == null) {
           System.out.println(passwordDTO.getCurrentPassword());
           System.out.println(passwordDTO.getNewPassword());
           throw new IllegalArgumentException("비밀번호는 null일 수 없습니다.");
       }

       // 현재 비밀번호가 올바른지 확인
       if (!passwordEncoder.matches(passwordDTO.getCurrentPassword(), user.getPassword())) {
           throw new RuntimeException("현재 비밀번호가 올바르지 않습니다!");
       }

       user.setPassword(passwordEncoder.encode(passwordDTO.getNewPassword()));
           return userRepository.save(user);
   }

   public User updateUser(String token, UserDTO updateDTO) {
       User user = getUserToken(token);

       if (updateDTO.getPhone() != null) {
           user.setPhone(updateDTO.getPhone());
       }
       if (updateDTO.getEmail() != null) {
           user.setEmail(updateDTO.getEmail());
       }
       if (updateDTO.getName() != null) {
           user.setName(updateDTO.getName());
       }

       return userRepository.save(user);
   }

   public UserDTO readUser(String token) {
       User user = getUserToken(token);
       return new UserDTO(
               user.getEmail(),
               user.getName(),
               user.getPhone()
       );
   }

   public void deleteUser(String token) {
       User user = getUserToken(token);
       userRepository.delete(user);
   }
}


UserJoinService에 있던 로직들을 가져왔고 서비스 로직을 분리!
UserService에는 현재 기본적인 CRUD만 담당한다!
또한 읽기에서 비밀번호와 아이디는 알려줄 필요가 없으므로 DTO를 사용하였다!
수정도 이메일, 핸드폰, 이름은 바로 수정이 가능!

비밀번호 변경은 비밀번호 인증후 변경가능
비밀번호 분실 시 재발급 로직은 PasswordResetService에 따로 구현!

추가
이메일을 이용한 재발급 로직말고
비밀번호를 변경하기 위한 changePassword추가!


UserAdditionlInfo

현재 *UserAdditionlInfoService 클래스에는 사진
(saveImage, updateImage)등이 위주로 있다.
마찬가지로 간단한 CRUD를 구현하고 사진 클래스는 따로 빼도록 하자

사진 클래스를 새로 만들고 옮겨오던중 저기
imageStorage = "storage/profile_images/";
와 같은 사진에 관련된 모든 기능들을 UserAdditionlService에서는 쓰기 싫었다.
기본적인 CRUD의 서비스만 남아있게 하고 싶고 사진 관련된건 사진 관련된 클래스에서 모두 처리하고 싶었다

public class UserAdditionalInfoUpdateDTO {
    private String nickname;
    private String profileImage;
    
    private String gender;

update를 위한 DTO 생성
성별은 고민하다가 추후에 성별에 따른 알고리즘으로 추천을 하려고 일단 집어넣었음!

@Service
@RequiredArgsConstructor
public class UserAdditionalInfoService {

    private UserAdditionalInfoRepository userAdditionalInfoRepository;
    private UserRepository userRepository;
    private ImageStorageService imageStorageService;
    private final JwtUtil jwtUtil;


    public UserAdditionalInfo saveAdditionalInfo(Long userId, UserAdditionalInfo userAdditionalInfo, MultipartFile profileImageFile) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found"));
        userAdditionalInfo.setUser(user);

        if(profileImageFile != null && !profileImageFile.isEmpty()) {

            String imageName = imageStorageService.saveImage(profileImageFile);
            String imageStorage = imageStorageService.imageStorage;
            userAdditionalInfo.setProfileImage(imageStorage + imageName);
        }
        return userAdditionalInfoRepository.save(userAdditionalInfo);
    }

    private UserAdditionalInfo getUserAdditionalToken(String token) {
        Claims claims = jwtUtil.extractAllClaims(token);
        String userId = claims.getSubject();

        return userAdditionalInfoRepository.findByUserId(Long.valueOf(userId))
                .orElseThrow(() -> new RuntimeException("유저 추가정보를 찾을 수 없습니다."));
    }

      public UserAdditionalInfoDTO readAdditionalInfo(String token) {
        UserAdditionalInfo userAdditionalInfo = getUserAdditionalToken(token);
        
        return new UserAdditionalInfoDTO(
                userAdditionalInfo.getNickname(),
                userAdditionalInfo.getProfileImage(),
                userAdditionalInfo.getGender()
        );
    }

    public UserAdditionalInfo updateAdditionalInfo(String token, UserAdditionalInfoUpdateDTO updateDTO) {
        UserAdditionalInfo userAdditionalInfo = getUserAdditionalToken(token);
        if (updateDTO.getGender() != null) {
            userAdditionalInfo.setGender(Gender.valueOf(updateDTO.getGender()));
        }
        if (updateDTO.getNickname() != null) {
            userAdditionalInfo.setNickname(updateDTO.getNickname());
        }
        return userAdditionalInfoRepository.save(userAdditionalInfo);
    }

    public void deleteAdditionalInfo(String token) {
        UserAdditionalInfo userAdditionalToken = getUserAdditionalToken(token);
        userAdditionalInfoRepository.delete(userAdditionalToken);
    }
}

마찬가지로 기본적인 CRUD를 담은
AddtionalService생성

@Service
public class ImageStorageService {

    private UserAdditionalInfoRepository userAdditionalInfoRepository;
    private UserRepository userRepository;
    private JwtUtil jwtUtil;

    public final String imageStorage = "storage/profile_images/";

    public ImageStorageService(UserAdditionalInfoRepository userAdditionalInfoRepository,
                                     UserRepository userRepository, JwtUtil jwtUtil) {
        this.userAdditionalInfoRepository = userAdditionalInfoRepository;
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;


        File directory = new File(imageStorage);
        if (!directory.exists()) {
            directory.mkdirs();
        }
    }

    public String saveImage(MultipartFile profileImageFile) {
        try {
            String originalFilename = profileImageFile.getOriginalFilename();
            String uniqueFileName = System.currentTimeMillis() + "_" + originalFilename;
            Path filePath = Paths.get(imageStorage + uniqueFileName);
            Files.write(filePath, profileImageFile.getBytes());

            return uniqueFileName;
        } catch (IOException e) {
            throw new RuntimeException(e + "저장에 실패하였습니다.");
        }
    }

    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);
        }
    }

}

사진관련 로직은 따로 서비스 클래스를 만들어서 사진에 대한 일들만 하도록 따로 분리하였음!!
로직 자체는 앞에서 짠 코드와 동일함


LoginController

AuthController ->
LoginController | UserController | UserAdditionalInfoController
로 쪼개서 변경하려고 한다.

기존 AuthController에는

  • join
  • profile
  • update
  • updateProfileImage
  • login
  • logout
  • additionalInfoJoin
  • refresh
  • extract-claims
    등 다양한 기능의 컨트롤러가 있었다..
    (솔직히 몇개쯤은 아무 생각없이 그냥 넣었다 ㅎㅎ)

아무튼 정신 차리고 다시 분리를 시작해보도록하자!

지금 이렇게 냅두면 나중에 리펙토링 하거나 코드에 기능을 추가할 때 지옥이다...!


UserAdditionalJoinController

package Grazie.com.Grazie_Backend.member.controller;

import Grazie.com.Grazie_Backend.Config.JwtUtil;
import Grazie.com.Grazie_Backend.member.*;
import Grazie.com.Grazie_Backend.member.entity.UserAdditionalInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RequestMapping("/users/additional-info")
@Controller
@RequiredArgsConstructor
public class UserAdditionalInfoController {

    private final ImageStorageService imageStorageService;
    private final AuthService authService;
    private final TokenManagementService tokenManagementService;
    private final JwtUtil jwtUtil;
    private final UserAdditionalInfoService userAdditionalInfoService;
    private final ObjectMapper objectMapper;
    private final UserProfileService userProfileService;



    @PostMapping("/{userId}/additionalInfoJoin")
    public ResponseEntity<UserAdditionalInfo> additionalInfoJoin(
            @PathVariable("userId") Long userId,
            @RequestParam("additionalInfo") String additionalInfoJson,
            @RequestPart("profileImage") MultipartFile profileImageFile) {

        try {
            UserAdditionalInfo userAdditionalInfo = objectMapper.readValue(additionalInfoJson, UserAdditionalInfo.class);
            UserAdditionalInfo saveAdditionalInfo = userAdditionalInfoService.saveAdditionalInfo(userId, userAdditionalInfo, profileImageFile);
            return ResponseEntity.ok(saveAdditionalInfo);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(null);
        }
    }



    @GetMapping("/ReadProfile")
    public ResponseEntity<?> readUserProfile(@RequestHeader("Authorization") String token) {
        try {
            // 토큰에서 bearer 부분 제거
            String jwtToken = token.substring(7);

            UserAdditionalInfo userAdditionalInfo = userAdditionalInfoService.readAdditionalInfo(jwtToken);

            return ResponseEntity.ok(userAdditionalInfo);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(400).body("프로필 조회 중 오류 발생");

        }
    }

    // 사진을 제외하고 업뎃
    @PutMapping("/update")
    public ResponseEntity<?> updateProfile(@RequestBody UserAdditionalInfoUpdateDTO userAdditionalInfoUpdateDTO, @RequestHeader("Authorization") String token) {
        try {
            // 토큰에서 bearer 부분 제거
            String jwtToken = token.substring(7);

            UserAdditionalInfo userAdditionalInfo = userAdditionalInfoService.updateAdditionalInfo(jwtToken, userAdditionalInfoUpdateDTO);
            return ResponseEntity.ok(userAdditionalInfo);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(400).body("프로필 업데이트 중 오류 발생");
        }
    }

    @PutMapping("/updateProfileImage")
    public ResponseEntity<?> updateProfileImage(@RequestPart("profileImage") MultipartFile profileImageFile, @RequestHeader("Authorization") String token) {
        try {
            String jwtToken = token.substring(7);
            UserAdditionalInfo additionalInfo = imageStorageService.updateImage(jwtToken, profileImageFile);
            return ResponseEntity.ok(additionalInfo);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.badRequest().body("프로필 이미지 업데이트 중 오류 발생");
        }
    }
}

@RequestMapping("/users/additional-info")
리퀘스트 매핑으로 앞에 additionaInfo 임을 알렸고
회원가입, 업데이트(사진과, 정보들 따로)

따로 한 이유는 보통 프로필 사진은 별개로 바꾸기 떄문이다!

문제는 read인데...additional에서read를 하면 회원 정보 전체가 나와버린다!

이제

  • join
    - additionalInfoJoin
    - readprofile
  • update
    - updateAdditional(추가)
    - updateProfileImage
  • login
  • logout
  • refresh
  • extract-claims

UserController

마찬가지로 UserController를 만들어보자~~!

@Controller
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {

    private final UserService userService;
    private final PasswordTokenRepository tokenRepository;
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    @PostMapping("/join")
    public ResponseEntity<User> join(@RequestBody User user) {
        try {
            User savedUser = userService.joinUser(user);
            return ResponseEntity.ok(savedUser);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    @GetMapping("/ReadProfile")
    public ResponseEntity<?> readUserProfile(@RequestHeader("Authorization") String token) {
        try {
            String jwtToken = token.substring(7);
            UserDTO userDTO = userService.readUser(jwtToken);
            return ResponseEntity.ok(userDTO);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(400).body("기본 프로필  조회 중 오류 발생");

        }
    }

    @PutMapping("/update")
    public ResponseEntity<?> updateProfile(@RequestBody UserDTO userDTO, @RequestHeader("Authorization") String token) {
        try {
            String jwtToken = token.substring(7);
            User user = userService.updateUser(jwtToken, userDTO);
            return ResponseEntity.ok(user);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(400).body("기본 프로필 업데이트 중 오류 발생");
        }
    }

    @DeleteMapping("/delete")
    public ResponseEntity<?> deleteUser(@RequestHeader("Authorization") String token) {
        String jwtToken = token.substring(7);
        userService.deleteUser(jwtToken);
        return ResponseEntity.ok().build();
    }
    
    @PostMapping("change-password")
    public ResponseEntity<?> TempPassword(@RequestParam("token") String token, @RequestBody UserDTO passwordDTO) {
        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 = passwordDTO.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("비밀번호 변경 실패!");
        }
    }
}

User에 대한 개인정보 CRUD + 비밀번호 변경 로직을 넣었다. 기존에 있던 EmailController 에서 변경 로직은 가져왔다! 위에 코드와 흡사해서 크게 다른점은 없다.

- join
- additionalInfoJoin
- readprofile
- update
- updateAdditional(추가)
- updateProfileImage

  • login
  • logout
  • refresh
  • extract-claims

정도 남았다!


AuthController

인증과 관련된 컨트롤러! 여기서는 로그인과 로그아웃을 처리해줄 것이다!

@Controller
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final AuthService authService;


    @PostMapping("/login")
    public ResponseEntity<LoginResponseDTO> login(@RequestBody LoginRequestDTO request) {

        return ResponseEntity.ok(authService.login(request.getUserid(), request.getPassword()));
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(@RequestBody String refreshToken) {
        authService.logOut(refreshToken);
        return ResponseEntity.noContent().build();
    }
}

드디어...! 몇백줄의 코드에서 30줄짜리로 변경되었다!!


MailController & Service

@RestController
@RequiredArgsConstructor
public class EmailController {
    
    private final PasswordResetService passwordResetService;

    @PostMapping("/request-temp-password")
    public ResponseEntity<?> requestTempPassword(@RequestBody UserDTO userDTO) {
        try {
            String email = userDTO.getEmail();
            passwordResetService.generateTempPassword(email);
            return ResponseEntity.ok("임시 비밀번호를 이메일로 발송했습니다.");
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("임시 비밀번호 발급 실패");
        }
    }
}
@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);
    }
}

메일 컨트롤러와 서비스 로직 앞에서 구현한 코드들을
메일에 대한 역할만 하도록 분리!


최종 변화!

삭제

  • UserProfileService (개인, 추가로 세세하게 분리)
  • UserProfileDTO
  • UserJoinService (서비스 로직에 통합)

생성 & 변화

  • userDTO

  • UserAdditionalnfoDTO

  • PasswordDTO

  • UserController (개인정보만)

  • UserService (개인정보 비지니스 로직만)

  • UserAdditionalInfoService (추가정보 비지니스 로직만)

  • UserAdditionalInfoController (추가정보만)

  • EmailController (메일만 남도록)

  • EmailService(비밀번호 로직 제외)

  • PasswordResetService (임시 비밀번호 재발급)

  • AuthController (인증과 관련된 것만 남기고 제외)

  • AuthService (로그인, 로그아웃만)

profile
자바를자바

0개의 댓글