새로운 컨벤션 규칙 정리
유지보수성을 고려하면서도 작업을 최소화하고 통일되보이는 좋은 코드로 발전하는 방향으로 개선했습니다. 조금 모자란 부분도 있지만 남은 시간과 팀들의 작업된 코드의 리팩토링 효율을 생각하여 적당히 정리하는 방법을 활용.
@Service
@RequiredArgsConstructor
public class ReviewService {
private final ReviewRepository reviewRepository;
private final StoreRepository storeRepository;
// 리뷰 작성
@Transactional
public ResponseEntity<StatusResponseDto> createReview(ReviewRequestDto reviewRequestDto, User user) {
return handleServiceRequest(() -> {
Store store = checkStoreExist(reviewRequestDto.getStoreId());
Review review = new Review(reviewRequestDto, user);
review.setStore(store);
reviewRepository.save(review);
return new StatusResponseDto("리뷰를 등록했습니다.", 200);
});
}
// 리뷰 수정
@Transactional
public ResponseEntity<StatusResponseDto> updateReview(Long id, ReviewRequestDto requestDto, User user) {
return handleServiceRequest(() -> {
Review review = checkReviewExist(id);
validateUserAuthority(user.getId(), review.getUser());
review.update(requestDto);
return new StatusResponseDto("리뷰가 수정되었습니다.", 200);
});
}
// 선택 리뷰 삭제
@Transactional
public ResponseEntity<StatusResponseDto> deleteReview(Long id, User user) {
return handleServiceRequest(() -> {
Review review = checkReviewExist(id);
validateUserAuthority(user.getId(), review.getUser());
reviewRepository.delete(review);
return new StatusResponseDto("리뷰가 삭제되었습니다.", 200);
});
}
// 가게 체크
private Store checkStoreExist(Long id) {
return storeRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 가게는 존재하지 않습니다.")
);
}
// 리뷰 체크
private Review checkReviewExist(Long id) {
return reviewRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("해당 리뷰는 존재하지 않습니다.")
);
}
// 동일 유저 체크
private void validateUserAuthority(Long reviewUserId, User user) {
if (!reviewUserId.equals(user.getId())) {
throw new IllegalArgumentException("리뷰 작성자만 접근할 수 있습니다.");
}
}
// 중복 코드 제거를 위한 메소드
private ResponseEntity<StatusResponseDto> handleServiceRequest(Supplier<StatusResponseDto> action) {
try {
return new ResponseEntity<>(action.get(), HttpStatus.OK);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto(ex.getMessage(), 400), HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
ex.printStackTrace();
return new ResponseEntity<>(new StatusResponseDto("서비스 요청 중 오류가 발생했습니다.", 500), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Java
복사
해당 커밋 내용은 전체 클래스의 컨벤션을 맞추는 작업하기 위한 기준으로 참고할것이고,
주요 포인트는 사소한 엔터의 위치나 주석등으로 가독성이 떨어지는 문제를 해결하고
모듈화를 통한 재사용성 상승, 지적되었던 검증 순서를 개선했고, 이를 통해 프로젝트 전체 유지보수성을 향상시키는 목적입니다.
•
진행할 전체 클래스 컨벤션 통일 규칙은 ReviewController, ReviewService, Review에 적용되어있습니다.
•
검증과 관련된 메소드들은 모듈화를 통해서 실제 비지니스 로직 구현부분은 간결하게 유지하고, 검증과 반환에 관련된 메소드로 모듈화된 부분들을 사용하면서 재사용성, 가독성, 유지보수성을 개선했습니다.
•
API별 반환 결과는 사용하고자 만들어두었던 StatusResponseDto를 통해서 msg와 statuscode가 클라이언트에 반환되도록 설정되어있습니다.
•
검증 관련 메소드들은 명명규칙에 따라서 명칭만으로 해당 검증 메소드의 정확한 의도를 파악 할 수 있도록 컨벤션을 전체적으로 적용하고자 합니다.
◦
예로 findStore도 좋지만, 가게의 존재여부를 체크하는 것이니 checkStoreExist() 와 같은 명명 컨벤션 적용하고자합니다.validateUserAuthority()의 경우 직관적인 명칭으로 판단되어 유지하기로 결정했습니다.
편의 메소드??
다음은 Review 엔티티클래스에 관련된 피드백 스터디 내용입니다.
Review-Store관계뿐만 아니라 다른 엔티티에서도 동일한 관계를 가지고 있다면 적용되어야 할 문제입니다.
우선 멘토님의 피드백 사항입니다.
연관관계 편의 메서드라고 검색하셔서 어떻게 처리해야하는지 한번 보시면 좋겠네요 양방향 연관관계에서 연관관계의 주인 측에서 데이터를 다뤄야 db에 변경사항이 반영되는건 맞지만, 현재 컨텍스트 내에서 사용하고있는 객체 자체에도 업데이트가 진행되어야 추후에 문제가 생기지 않습니다.
Review 엔티티와 Store 엔티티는 1:N 연관관계며 양방향 관계이기도 합니다.
Review.java 엔티티클래스에서 setStore() 메소드를 통해 아래와 같이 양방향 연관관계를 맺고 있습니다.
@Table(name = "reviews")
public class Review extends TimeStamped {
...
@JsonIgnore
@JoinColumn(name = "store_id")
@ManyToOne(fetch = FetchType.LAZY)
private Store store;
...
public void setStore(Store store) {
this.store = store;
}
Java
복사
Review 엔티티클래스는 Store 엔티티클래스와 양방향 관계에 있습니다. (리뷰:가게 <=> N:1 양방향)
서로의 필드를 조회 할 수 있는 상태라는 말과 같습니다.
@OneToMany(mappedBy = "store", orphanRemoval = true)
private List<Review> reviewList = new ArrayList<>();
Java
복사
하지만 문제가 있습니다.
일관성을 유지하지 못할 가능성이 있습니다.
따라서 아래와 같은 세터 메소드를 작성해서 일관성을 보완해야 합니다.
public void setStore(Store store) {
this.store = store;
if (!store.getReviewList().contains(this)) {
store.getReviewList().add(this);
}
}
Java
복사
위 메소드를 설명하면
리뷰:가게 = N:1이면서 양방향인 경우에
리뷰(N)는 키주인인 가게(1)에서 리뷰 목록으로 담기게 되는데
실제로 List에 존재하는지 여부를 체크하고
없는 경우 리뷰객체본인 자체를 리뷰리스트에 추가시키면서 양쪽을 항상 동일하게 유지하게 됩니다.
따라서 리뷰(N)에서는 위 setter 메소드를 통해
리뷰 객체 본인이 들어 갈 가게의 리뷰목록에서 자기자신을 체크하고 없으면 넣는 로직 작성해야 일관성이 유지되고
DB뿐만 아니라 Java의 시점에서도 리뷰객체와 리뷰리스트객체간의 일관성이 유지됩니다.
그런데 여기서 추가적으로 살펴볼 문제가 있습니다.
위 기본 setStore 메서드를 아래와 같이 보완해야 합니다.
public void setStore(Store store) {
this.store = store;
if (store != null && !store.getReviewList().contains(this)) {
store.getReviewList().add(this);
}
}
Java
복사
하지만 코드의 간결성을 유지해야 한다.
위 방법은 수동적이고, 엔티티의 속성을 통해서 해결할 수 있다.
Store.java 엔티티
@OneToMany(mappedBy = "store", orphanRemoval = true)
private List<Review> reviewList = new ArrayList<>();
Java
복사
이미 Store엔티티에서는 리뷰에 대하여 orphanRemoval = true로 일관성을 유지하고 있다.
Review.java 엔티티
@JsonIgnore
@JoinColumn(name = "store_id")
@ManyToOne(fetch = FetchType.LAZY)
private Store store;
Java
복사
Review엔티티에서는 @JsonIgnore를 통해서 직렬화 과정에서 무한루프를 방지하고 있다.
Review.java의 하단 편의 메소드 멤버
//편의 메소드(Helper Method)를 대신하여 orphanRemoval로 일관성을 유지했다.
//하지만 JSON변환과정의 직렬화 무한루프를 방지하기 위해서는
//public void setStore(Store store) {
// this.store = store;
// if (!store.getReviewList().contains(this)) {
// store.getReviewList().add(this);
// }
//}
Java
복사
하단 편의 메소드는 orphanRemoval = true로 일관성을 유지하는 것과 중복되기 때문에 제거한다. 이에 따라 기본적인 Setter 형태의 메소드 조차 필요가없다.