Light Blue Pointer
본문 바로가기
Developing/개발일지

2023-12-15, Today I Learned

by Greedy 2023. 12. 15.

오늘 한 일

todolist 프로젝트

1.  try catch문으로 가득한 controller -> @RestControllerAdvice 사용해서 예외 공동화 처리

2. service단의 interface와 구현체 분리함

 

Q. 왜 분리해야 할까?

 

좀 찾아봄

Purpose of service interface class in spring boot

1. The first and most important reason is testability. You can create mocks of service interface and test your code easily, if you cannot create mocks using a mocking library then you can create test stubs.

2. we can achieve loose coupling between Controller and Service layer. Suppose you want to entirely change the implementation of service, you can create new service implementation and inject that implementation by injecting new bean by qualifier name

 

어떤 사람은 자기 회사가 왜 Service 구현체와 인터페이스를 분리하는지 모르겠다고 함

답변들 :

Your company is following the SOLID principles and targeting an interface rather than concrete class adds zero overhead to the program. What they're doing is a very moderate amount of extra work that can pay back volumes when the assumptions that you are making end up being false...and you can never tell what assumption that's going to be. What you're recommending while slightly less work, can cost many, many, many hours of refactoring that could have just been done ahead of time by simply following basic OO design principles.

 

Given your question I assume that the reasons for this kind design are not documented.

Unjustified usage of an interface with single implementation is plain wrong since this violates YAGNI. In my experience, this also has been pretty damaging maintenance-wise, mostly because methods implementing interface are forced to be unnecessarily public. After you gather more refactoring experience, you'll probably learn to appreciate modest charm of private and package private access modifiers allowing one to focus attention within a single class / package.

As for undocumented justified usage of this idiom, it is in my eyes about as bad - first of all because it doesn't allow a maintainer to "tell good from bad". As a result, this leaves one between not quite appealing options. Options are, either you keep the questionable public stuff as-is, thus repeatedly decreasing productivity every time you need to change the code, or you make risky changes by blindly "collapsing" the single-implementation into easier to maintain POJO - exposing self to the risk to painfully discover that this is wrong way, that some other (God forbid, remote) module out of your sight expects the interface.

 

Java interfaces aren't just about mockability (though of course, that is a factor). An interface exposes a public contract for your service, without implementation details such as private methods or attributes.

Note that "public" when using internal services (as in your case) refers to the actual users of said services, including -- but not limited to -- other programmers and internal services!

 

-> 인터페이스와 구현체를 나눴을때

1. Mock으로 테스트하기 편하다

2. Controller와 Service의 강한 결합을 방지할 수 있다고 함(수정이 용이하다는 거 같음)

3. SOLID 원칙을 지키는 방법이다

4. POJO를 유지할 수 있다-> what's POJO -> Plain Old Java Object

 

3,4는 지금 당장은 와닿지 않는데 1,2는 알겠음

 

아무튼 Service를 분리해봤다

public interface CommentService {

    /*
     * Comment 생성
     * @param requestDto Comment 생성 요청정보
     * @param user Comment 생성 요청자
     * @return Comment 생성 결과
     */
    public CommentResponseDTO createComment(CommentRequestDTO dto, User user);

    /*
     * Comment 수정
     * @param requestDto Comment 수정 요청정보
     * @param user Comment 수정 요청자
     * @return Comment 수정 결과
     */
    public CommentResponseDTO updateComment(Long commentId, CommentRequestDTO commentRequestDTO, User user);

    /*
     * Comment 삭제
     * @param id 삭제할 Comment id
     * @param user Comment 삭제 요청자
     * @return Comment 삭제 결과
     */
    public void deleteComment(Long commentId, User user);

}

 

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService{
    private final CommentRepository commentRepository;
    private final TodoServiceImpl todoServiceImpl;

    public CommentResponseDTO createComment(CommentRequestDTO dto, User user) {
        Todo todo = todoServiceImpl.getTodo(dto.getTodoId());

        Comment comment = new Comment(dto);
        comment.setUser(user);
        comment.setTodo(todo);

        commentRepository.save(comment);

        return new CommentResponseDTO(comment);
    }

    @Transactional
    public CommentResponseDTO updateComment(Long commentId, CommentRequestDTO commentRequestDTO, User user) {
        Comment comment = getUserComment(commentId, user);

        comment.setText(commentRequestDTO.getText());

        return new CommentResponseDTO(comment);
    }

    public void deleteComment(Long commentId, User user) {
        Comment comment = getUserComment(commentId, user);

        commentRepository.delete(comment);
    }

    private Comment getUserComment(Long commentId, User user) {
        Comment comment = commentRepository.findById(commentId)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글 ID 입니다."));

        if(!user.getId().equals(comment.getUser().getId())) {
            throw new RejectedExecutionException("작성자만 수정할 수 있습니다.");
        }
        return comment;
    }
}

 

@RequiredArgsConstructor annotation안 먹혀서 Controller에다 수동으로 만들어줌

@RequestMapping("/api/comments")
@RestController
public class CommentController {

    private final CommentService commentService;

    public CommentController(CommentServiceImpl commentServiceImpl){
        this.commentService = commentServiceImpl;
    }

    @PostMapping
    public ApiResponseDTO<CommentResponseDTO> postComment(@RequestBody CommentRequestDTO commentRequestDTO, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        CommentResponseDTO responseDTO = commentService.createComment(commentRequestDTO, userDetails.getUser());
        return new ApiResponseDTO<>(HttpStatus.CREATED.value(), "Comment 작성 성공", responseDTO);
    }

    @PutMapping("/{commentId}")
    public ApiResponseDTO<CommonResponseDTO> putComment(@PathVariable Long commentId, @RequestBody CommentRequestDTO commentRequestDTO, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        CommentResponseDTO responseDTO = commentService.updateComment(commentId, commentRequestDTO, userDetails.getUser());
        return new ApiResponseDTO<>(HttpStatus.OK.value(), "Comment 수정 성공", responseDTO);
    }


    @DeleteMapping("/{commentId}")
    public ApiResponseDTO<Void> deleteComment(@PathVariable Long commentId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        commentService.deleteComment(commentId, userDetails.getUser());
        return new ApiResponseDTO<>(HttpStatus.OK.value(), "Comment 삭제 성공");
    }
}

 

Board 프로젝트

 

'Developing > 개발일지' 카테고리의 다른 글

2023-12-21, Today I Learned  (1) 2023.12.21
2023-12-18, Today I Leanred  (0) 2023.12.18
2023-12-14, Today I Learned  (0) 2023.12.14
2023-12-12, Today I Learned  (0) 2023.12.12
2023-12-11, Today I Learned  (0) 2023.12.11