포인트 시스템 리팩토링

금은체리·2024년 6월 10일
0

사용자와 기업 사용자가 다양한 활동(예: 리뷰 작성, 회원가입 등)을 통해 포인트를 적립할 수 있도록 하는 포인트 시스템을 구현했습니다. 기존 시스템에서는 포인트 적립 로직이 여러 서비스 클래스(AuthService, ReviewService 등)에 분산되어 있어 코드 중복이 발생하고 유지보수성이 떨어졌습니다. 이러한 문제를 해결하기 위해 단일 책임 원칙(Single Responsibility Principle, SRP)과 관심사 분리(Separation of Concerns, SoC)를 적용하여 포인트 시스템을 리팩토링했습니다.

문제점

  1. 코드 중복: 포인트 적립 로직이 여러 서비스 클래스에 분산되어 있어 동일한 코드가 여러 곳에서 중복되고 있었습니다.
  2. 유지보수 어려움: 포인트 로직이 여러 클래스에 분산되어 있어, 포인트 관련 로직을 수정하거나 추가할 때 여러 클래스를 수정해야 했습니다.
  3. 확장성 부족: 새로운 유형의 사용자(예: 다른 종류의 회사 사용자)가 추가될 때마다 각 서비스 클래스에 포인트 적립 로직을 추가해야 했습니다.

목표

  • 단일 책임 원칙(SRP)을 적용하여 포인트 적립 로직을 하나의 클래스에 집중시킵니다.
  • 관심사 분리(SoC)를 통해 서비스 클래스들이 각각의 주요 책임에만 집중할 수 있도록 합니다.
  • 포인트 시스템의 유지보수성과 확장성을 향상시킵니다.

해결책

  1. 공통 인터페이스 정의: UserCompanyUser가 공통으로 구현할 CommonPoint 인터페이스를 정의했습니다.

    public interface CommonPoint {
        Point getPoint();
        void setPoint(Point point);
    }
  2. 인터페이스 구현: UserCompanyUser 클래스가 CommonPoint 인터페이스를 구현하도록 수정했습니다.

    public class User implements CommonPoint {
        private Point point;
        @Override
        public Point getPoint() {
            return point;
        }
        @Override
        public void setPoint(Point point) {
            this.point = point;
        }
    }
    
    public class CompanyUser implements CommonPoint {
        private Point point;
        @Override
        public Point getPoint() {
            return point;
        }
        @Override
        public void setPoint(Point point) {
            this.point = point;
        }
    }
  3. 포인트 서비스 리팩토링: 포인트 관련 로직을 PointService 클래스에 집중시켰습니다.

    @Service
    @RequiredArgsConstructor
    public class PointService {
    
        private final PointRepository pointRepository;
        private final SavedPointRepository savedPointRepository;
    
        @Transactional
        public void earnPoints(CommonPoint holder, SpType spType) {
            int points = getPointsByType(spType);
            Point point = holder.getPoint();
            if (point == null) {
                point = new Point(points);
            } else {
                point.setPoint(point.getPoint() + points);
            }
            pointRepository.save(point);
            holder.setPoint(point);
    
            SavedPoint savedPoint = SavedPoint.builder()
                .point(point)
                .spAmount(points)
                .spBalance(point.getPoint())
                .spType(spType)
                .build();
            savedPointRepository.save(savedPoint);
        }
    
        private int getPointsByType(SpType spType) {
            return switch (spType) {
                case REVIEW, BOARD -> 100;
                case SIGNUP -> 500;
                default -> 0;
            };
        }
    }
  4. 서비스 클래스 리팩토링: AuthServiceReviewService에서 포인트 관련 로직을 제거하고, 대신 PointService를 사용하도록 수정했습니다.

    @Service
    @RequiredArgsConstructor
    public class AuthService {
    
        private final UserRepository userRepository;
        private final CompanyUserRepository companyUserRepository;
        private final PointService pointService;
    
        @Transactional
        public void signup(UserSignupRequestDTO signupRequest) {
            validateUserSignupRequest(signupRequest.getEmail(), signupRequest.getPassword(),
                    signupRequest.getConfirmPassword(), signupRequest.isAgreeToTerms());
    
            User user = User.builder()
                    .username(signupRequest.getUsername())
                    .nickname(signupRequest.getNickname())
                    .email(signupRequest.getEmail())
                    .password(passwordEncoder.encode(signupRequest.getPassword()))
                    .loginType(signupRequest.getLoginType())
                    .build();
    
            userRepository.save(user);
            pointService.earnPoints(user, SpType.SIGNUP);
        }
    
        @Transactional
        public void cpSignup(CpUserSignupRequestDTO requestDTO) {
            validateCpSignupRequest(requestDTO.getCpEmail(), requestDTO.getCpPassword(),
                    requestDTO.getCpConfirmPassword(), requestDTO.isAgreeToTerms());
    
            CompanyUser companyUser = CompanyUser.builder()
                    .hiringStatus(requestDTO.getHiringStatus())
                    .employeeCount(requestDTO.getEmployeeCount())
                    .foundationDate(requestDTO.getFoundationDate())
                    .description(requestDTO.getDescription())
                    .cpNum(encryptionService.encryptCpNum(requestDTO.getCpNum()))
                    .cpName(requestDTO.getCpName())
                    .cpUsername(requestDTO.getCpUsername())
                    .cpEmail(requestDTO.getCpEmail())
                    .cpPhoneNumber(requestDTO.getCpPhoneNumber())
                    .cpPassword(passwordEncoder.encode(requestDTO.getCpPassword()))
                    .build();
    
            companyUserRepository.save(companyUser);
            pointService.earnPoints(companyUser, SpType.SIGNUP);
        }
    }
    
    @Service
    @RequiredArgsConstructor
    public class ReviewService {
    
        private final ReviewRepository reviewRepository;
        private final CompanyUserRepository companyUserRepository;
        private final UserRepository userRepository;
        private final PointService pointService;
    
        @Transactional
        public ReviewCreationResponseDTO createReview(Long userId, Long cpUserId, ReviewCreationRequestDTO requestDto) {
    
            User user = userRepository.findById(userId)
                    .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND));
            CompanyUser companyUser = companyUserRepository.findById(cpUserId)
                    .orElseThrow(() -> new ApiException(ErrorCode.COMPANY_USER_NOT_FOUND));
    
            Review review = Review.builder()
                    .user(user)
                    .cpUser(companyUser)
                    .reviewTitle(requestDto.getReviewTitle())
                    .reviewContent(requestDto.getReviewContent())
                    .rating(requestDto.getRating())
                    .isPrivate(true)
                    .build();
            review = reviewRepository.save(review);
    
            pointService.earnPoints(user, SpType.REVIEW);
    
            return new ReviewCreationResponseDTO(
                    review.getReviewId(),
                    requestDto.getCpUserId(),
                    review.getReviewTitle(),
                    review.getReviewContent(),
                    review.getRating(),
                    review.getIsPrivate()
            );
        }
    }

결과

  • 코드 중복 제거: 포인트 관련 로직이 PointService에 집중되어 코드 중복이 제거되었습니다.
  • 유지보수성 향상: 포인트 관련 로직이 한 곳에 모여 있어 수정이나 확장이 용이해졌습니다.
  • 확장성 확보: 새로운 유형의 사용자나 포인트 적립 이벤트가 추가될 때, PointService만 수정하면 되므로 확장성이 향상되었습니다.
profile
전 체리 알러지가 있어요!

0개의 댓글