Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Tags
- sqld자격증합격
- 노베이스부트캠프
- 격파르타합격후기
- java참조자료형
- java set 저장
- 인터프린터언어
- 코딩부트캠프후기
- 격파르타장점
- 비전공자sqld
- java알고리즘문제풀이
- java 자료구조 활용
- 격파르타비전공자
- java알고리즘
- 컴파일
- java set 출력
- java list 저장
- java map 출력
- java map 저장
- 항해99후기
- 작은수제거하기
- javaJVM
- javaJRE
- java최솟값구하기
- java map
- java list 출력
- java기본자료형
- 항해15기
- 격파르타후기
- 프로그래머스제일작은수
- 프로그래머스
Archives
- Today
- Total
코딩과 결혼합니다
232025 - 코드리팩토링 (Alarm) 본문
728x90
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들도 더 나은 방향으로 코드 리팩토링을 한 뒤에 전체적인 부분을 다시 한 번 체크해보는 시간을 가져봐야겠다.
'코딩과 매일매일♥ > Seoulvival' 카테고리의 다른 글
[Seoulvival] 트러블 슈팅 : EC2 인스턴스 연결 실패 + IMDSv2 (1) | 2023.11.23 |
---|---|
230920 - 코드 리팩토링(클래스명 변경하기 + 도메인 줄이기) (0) | 2023.09.20 |
230918 - 코드 리팩토링(폴더 구조 변경하기) (0) | 2023.09.18 |
230906 - @Transactional을 걸어도 더티체킹이 안돼! (0) | 2023.09.06 |
230902 - (코드리팩토링) api 6개를 3개로 합치기!! (0) | 2023.09.02 |