코딩과 결혼합니다

232025 - 코드리팩토링 (Alarm) 본문

코딩과 매일매일♥/Seoulvival

232025 - 코드리팩토링 (Alarm)

코딩러버 2023. 9. 25. 16:38
728x90

1. 파일 구조 변경하기

2. 클래스명 변경하기 + 도메인 줄이기

3. Domain - Alarm  📌


1. String alarmCategory를 EnumType으로 받기

alarmCategory를 activity와 hashtag 이렇게 두 가지로 받고 있다. 무엇을 받을지 명확한 상태에서는 이 두 가지 외에 잘못된 값이 오지 않도록 EnumType으로 설정해 두는 게 좋겠다는 판단을 하였다.
또한 컴파일 에러 체크로 인해 잘못된 값 입력 시 바로 오류를 찾아낼 수 있다는 이점도 있다.
EnumType으로 변경

    //활동 및 해시태그 알림 조회
    @GetMapping("/activity")
    public AlarmsResponseDto getNotification(@RequestParam int page,
                                             @RequestParam int size,
                                             @RequestParam AlarmCategoryType alarmCategory,
                                             @AuthenticationPrincipal UserDetailsImpl userDetails){
        User user = userDetails.getUser();
        return alarmService.getNotification(page-1, size, alarmCategory,  user);
    }
    
    ===============================================================================================
    
    public enum AlarmCategoryType {
    ACTIVITY,
    HASHTAG
}

 

2. AlarmResponseDto 하나로 줄이기

//합치기 전

  public AlarmResponseDto(Long id, Long postId, AlarmEventType alarmEventType, String text, Boolean isRead, LocalDateTime registeredAt, String userImg) {
        this.id = id;
        this.postId = postId;
        this.alarmEventType = alarmEventType;
        this.text = text;
        this.isRead = isRead;
        this.registeredAt = registeredAt;
        this.userImg = userImg;
    }

    public AlarmResponseDto(Long id, Long postId, AlarmEventType alarmEventType, String text, Boolean isRead, LocalDateTime registeredAt, String userImg, String hashTagName) {
        this.id = id;
        this.postId = postId;
        this.alarmEventType = alarmEventType;
        this.text = text;
        this.isRead = isRead;
        this.registeredAt = registeredAt;
        this.userImg = userImg;
        this.hashTagName = hashTagName;
    }
    
    
//합친 후
    public AlarmResponseDto(Alarm alarm, boolean isHashTag) {
        this.id = alarm.getId();
        this.postId = alarm.getPost().getId();
        this.alarmEventType = alarm.getAlarmEventType();
        this.text = alarm.getNotificationMessage();
        this.isRead = alarm.getIsRead();
        this.registeredAt = alarm.getRegisteredAt();
        this.userImg = alarm.getUserImg();
        if (isHashTag){
            this.hashTagName = alarm.getHashtagName();
        }
    }

1.  Alarm객체 하나와 boolean 값으로 줄어들면서, 중복된 코드를 제거할 수 있고 유지보수에 도움이 된다.
2. 유연성 증가 : 알람 객체의 내부 구조가 변경되도라도, AlarmResponseDto의 생성자는 그대로 유지될 수 있다.
3. 객체 지향 프로그래밍 : Alarm 객체의 필드값을 직접 접근하는 대신 Alarm 클래스에서 제공하는 getter메서드를 사용함으로써 캡슐화 원칙을 지킨다.

 

3. Enum 비교할 때 ==연산자로 & 중복 코드 제거

위의 코드에서는 알람 이벤트 타입에 따라 필터링을 두 번 진행하고 있다. 이를 개선하기 위해 각 알람카테고리 타입에 따른 필터링 조건을 메서드로 분리하여 중복을 줄여 가독성을 높였다.

Predicate는 Java8에서 도입된 함수형 인터페이스 중 하나로, 입력을 받아 boolean 값을 반환하는 함수를 표현한다. filterPredicate는 Alarm 객체를 입력으로 받아, 해당 알람이 특정 조건을 만족하는지 판단하는 역할을 한다.


4. 가독성을 떨어뜨리는 부분은 메서드로 분리

    //해시태그 구독
    public MessageResponseDto subscribeHashtag(HashtagRequestDto requestDto, Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다."));

        String hashtagName = requestDto.getHashtagName();

        // 해시태그 이름이 #으로 시작하지 않거나, #이 두 개 이상 포함되어 있거나, 5글자를 초과하면 예외를 발생시킵니다.
        if (!hashtagName.startsWith("#") || hashtagName.chars().filter(ch -> ch == '#').count() > 1 || hashtagName.length() > 5) {
            throw new IllegalArgumentException("잘못된 해시태그 형식입니다.");
        }

        // 이미 구독한 해시태그인 경우 예외를 발생시킵니다.
        if (subscribeHashtagRepository.existsByUserAndHashtag(user, requestDto.getHashtagName())) {
            throw new IllegalArgumentException("이미 구독한 해시태그입니다.");
        }

        SubscribeHashtag subscribeHashtag = new SubscribeHashtag(user, hashtagName);
        subscribeHashtagRepository.save(subscribeHashtag);

        return new MessageResponseDto("해시태그 구독 완료");
    }
    //해시태그 구독
    public MessageResponseDto subscribeHashtag(HashtagRequestDto requestDto, Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException("사용자가 존재하지 않습니다."));

        String hashtagName = requestDto.getHashtagName();

        //validate the hashtag name
        validateHashtagName(hashtagName);

        //Check if the user has already subscribed to the hashtag.
        checkIfAlreadySubscribed(user, hashtagName);

        SubscribeHashtag subscribeHashtag = new SubscribeHashtag(user, hashtagName);
        subscribeHashtagRepository.save(subscribeHashtag);

        return new MessageResponseDto("해시태그 구독 완료");
    }
복잡한 로직을 별도의 메서드로 분리하여 메서드가 어떻게 기능을 하는지 더 이해하기가 쉬우며, 각 메서드가 한 가지 책임만 가지게 되어 코드의 유지보수성 또한 향상된다.

 

5. 예외를 동일하게 처리 + 메시지 클래스를 따로 만들어 관리

public class CustomRuntimeException extends RuntimeException {
    private final HttpStatus status;

    public CustomRuntimeException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}

RuntimeException을 상속받는 CustomRuntimeException이 이미 존재해 이를 사용하였다.(다른 팀원이 만들어 둔 것) 예외를 동일하게 처리함으로 가독성과 유지보수성이 향상되었다.

 

public class Message {
    public static final String HASHTAG_SUBSCRIPTION_CANCELLATION = "해시태그 구독 취소를 완료했습니다.";
    public static final String HASHTAG_SUBSCRIPTION_COMPLETED = "해시태그 구독 취소를 완료했습니다.";
    public static final String SUBSCRIPTION_CHANGE_COMPLETED = "구독 변경을 완료했습니다.";
    public static final String ALARM_READ_PROCESSING = "알림을 읽음 처리했습니다.";
	...
}


public class ErrorMessage {
    public static final String USER_NOT_FOUND = "사용자가 존재하지 않습니다.";
    public static final String INVALID_HASHTAG = "잘못된 해시태그 형식입니다.";
    public static final String ALREADY_SUBSCRIBED = "이미 구독한 해시태그 입니다.";
    public static final String ALARM_TYPE_NOT_FOUND = "알림 타입이 존재하지 않습니다.";
    public static final String USER_NOT_SUBSCRIBE_HASHTAG = "사용자가 해당 해시태그를 구독하지 않았습니다.";
    public static final String THIS_NOTIFICATION_NOT_FOUND = "해당 알림은 존재하지 않습니다.";
    public static final String NO_ACCESS_PERMISSION = "알림에 대한 엑세스 권한이 없습니다.";
    ...
}
메시지를 따로 관리해 주면 나중에 메시지의 내용이 바뀌게 될 때 이 클래스에서만 바꿔주면 되므로 유지보수에 용이하다. 또한 메시지가 중앙에서 관리되므로, 같은 메시지를 여러 번 작성하지 않아도 되고 일관성을 유지할 수 있다.

6. 알림 읽음 처리하는 부분을 트랜잭셔널로 처리

//수정 전
    public MessageResponseDto markNotificationAsRead(@PathVariable Integer notificationId, User user) {
        // 알림 조회
        Alarm alarm = alarmRepository.findById(notificationId)
                .orElseThrow(() -> new CustomRuntimeException(ErrorMessage.THIS_NOTIFICATION_NOT_FOUND, HttpStatus.NOT_FOUND));

        // 사용자 인증 및 권한 검사
        if (!alarm.getUser().getId().equals(user.getId())) {
            throw new CustomRuntimeException(ErrorMessage.NO_ACCESS_PERMISSION, HttpStatus.FORBIDDEN);
        }

        // 알림을 읽음 처리
        alarm.setIsRead(true);
        alarmRepository.save(alarm);

        return new MessageResponseDto(Message.ALARM_READ_PROCESSING, HttpStatus.OK);
    }
}


//수정 후
    @Transactional
    public MessageResponseDto markNotificationAsRead(@PathVariable Integer notificationId, User user) {
        // 알림 조회
        Alarm alarm = alarmRepository.findById(notificationId)
                .orElseThrow(() -> new CustomRuntimeException(ErrorMessage.THIS_NOTIFICATION_NOT_FOUND, HttpStatus.NOT_FOUND));

        // 사용자 인증 및 권한 검사
        if (!alarm.getUser().getId().equals(user.getId())) {
            throw new CustomRuntimeException(ErrorMessage.NO_ACCESS_PERMISSION, HttpStatus.FORBIDDEN);
        }

        // 알림을 읽음 처리
        alarm.setIsRead(true);

        return new MessageResponseDto(Message.ALARM_READ_PROCESSING, HttpStatus.OK);
    }
}
원자성 : 알림 읽음 처리와 상태 저장이 원자적으로 수행되어 일관성을 유지하기 위함.
일관성 : 중간에 오류가 발생할 경우 알림은 읽음 처리되었지만 상태를 업데이트되지 않는 경우가 발생한다.
격리성 : 변경 사항이 커밋되기 전까지 다른 사용자에게 보이지 않는다. 이로 인해 데이터의 무결성과 동시성
              제어가 가능해진다.
영속성 : 완료된 트랜잭션은 영구적으로 저장된다. 시스템 장애 또는 전원 손실과 같은 문제가 발생해도 커밋된 
              데이터는 손실되지 않는다.

트랜잭션이 없다면 각 단계마다 개별적인 save호출로 인한 여러 번의 DB 접근 및 I/O비용이 발생하게 된다. 
트랜잭션 내에서 모든 작업들은 하나의 논리적인 단위로 묶여서 한 번에 커밋하거나 롤백할 수 있으므로 성능상의 이점도 있다.

CustomRuntimeException은 RuntimeException의 하위 클래스로 스프링이 자동으로 롤백 처리를 수행한다.

오늘은 Alarm 부분과 global 패키지쪽을 수정 해주었다. 다음 domain들도 더 나은 방향으로 코드 리팩토링을 한 뒤에 전체적인 부분을 다시 한 번 체크해보는 시간을 가져봐야겠다.