코드스쿼드 2 - (DTO 변환, 체크예외, VIEW 에 의존하는 엔티티)

Alex·2024년 7월 1일
0

리팩토링

목록 보기
3/17

DTO 변환

클라이언트에서 받은 dto를 엔티티 객체로 변환할 때 어느 레벨에서 하는 것이 적합할까요? (service / controller)
였다.

저는 서비스 레이어에서 변환해주는걸 선호하는 편입니다! 저는 가능한 presentation layer(controller)는 view와 service 호출, 예외 처리 정도의 책임만 가지게 설계하는 편입니다. dto를 엔티티로 만드는 것도 비즈니스 로직의 일부라고 보는 편이라 dto -> 엔티티 로직은 서비스에서 처리하는 걸 선호합니다!

처음에는 코드를 급하게 구현하느라 컨트롤러단에서 여러 작업을 수행했다.

이후로 팀원들과 이야기를 나누면서 dto를 어디서 작업해야 할지?를 논의했었다.

이러한 맥락에서 나온 질문이다.


public ReservationResponse toEntity() {
        ReservationResponse reservationResponse = new ReservationResponse();
        reservationResponse.setUser(user.toEntity());
        reservationResponse.setAccommodationId(AccommodationMapper.toResponse(accommodation));
        reservationResponse.setRegisteredAt(registeredAt);
        reservationResponse.setAmount(amount);
        reservationResponse.setPersonCount(personCount);
        reservationResponse.setCheckIn(checkIn);
        reservationResponse.setCheckOut(checkOut);

        return reservationResponse;
    }

이후로는 이런 방식으로 엔티티 내부에 toEntity같은 메서드를 만들었다. (+정적 메서드를 사용했다) 서비스 단에서 메서드를 호출하는 식으로 변경한 것이다.

컨트롤러는 서비스와 뷰 호출, 예외처리에 집중하려고 했다.

air-dnb 프로젝트를 하면서는 dto로 변환하는 과정에서 컨트롤러와 서비스의 의존성도 줄이려고 두 단계에 걸쳐서 dto를 만들었었다.

public class AccommodationMapper {

    public static AccommodationSaveServiceRequest toSaveService(AccommodationSave saveRequest, List<MultipartFile> files) {
        return new AccommodationSaveServiceRequest(
                saveRequest.getName(),
                saveRequest.getPrice(),
                saveRequest.getAddress(),
                saveRequest.getMaxCapacity(),
                saveRequest.getRoomCount(),
                saveRequest.getBedCount(),
                saveRequest.getDescription(),
                saveRequest.getAmenity(),
                files,
                saveRequest.getAccommodationType(),
                saveRequest.getHashtagContents()
        );
    }

    public static AccommodationUpdateServiceRequest toUpdateService(AccommodationUpdate updateRequest, List<MultipartFile> files) {
        return new AccommodationUpdateServiceRequest(
                updateRequest.getName(),
                updateRequest.getPrice(),
                updateRequest.getAddress(),
                updateRequest.getMaxCapacity(),
                updateRequest.getRoomCount(),
                updateRequest.getBedCount(),
                updateRequest.getDescription(),
                updateRequest.getAmenity(),
                files,
                updateRequest.getAccommodationType(),
                updateRequest.getHashtagContents()
        );
    }

    public static Accommodation toEntity(AccommodationSaveServiceRequest serviceRequest) {
        return new Accommodation(
                serviceRequest.getName(),
                serviceRequest.getPrice(),
                serviceRequest.getAddress(),
                serviceRequest.getMaxCapacity(),
                serviceRequest.getRoomCount(),
                serviceRequest.getBedCount(),
                serviceRequest.getDescription(),
                serviceRequest.getAmenity(),
                serviceRequest.getAccommodationType()
        );
    }
    

컨트롤러에서 받는 폼 dto를 서비스에 보내면, service와 controller간의 의존성이 생긴다는 팀원 의견을 듣고 이런 식으로 코드를 리팩토링했다.

다만, 서비스와 컨트롤러 의존성이 어떤 점에서 문제인지를 명확하게 이해하지 못했다. 이 부분은 더 공부해봐야 할 거 같다.

위 답변을 보면 DTO를 분리하는 게 성능적인 영향을 크게 주지는 않는 거 같다.

반대로, 컨트롤러가 서비스에 의존하는 방식은 무관하다고 한다.

OKKY를 찾아보니 위와 같은 내용이 있었다. 컨트롤러 API를 변경하기는 쉽지 않을 수 있으니, 서비스 로직이 변할 때마다 이에 맞춰 대응할 수 있게 서비스단에서의 DTO를 변경시킨다는 말이다.

+의존성을 줄이기 위해서는 dto의 필드값이 적다면
service에는 그냥 필드로 보내는 것도 좋은 방법이라고 한다.

예외처리


package com.codesquad.team3.issuetracker.global.exceptions;

public class NoSuchRecordException extends Exception{

여기서 체크 예외를 사용하신 이유가 있을까요?
예외를 만들 때 RuntimeException을 상속하는 것과 Exception을 상속하는 차이는 뭘까요?

라는 리뷰가 달렸다.

저는 메서드 시그니쳐에 어떤 예외가 발생할 수 있는지 붙어있는게 직관적으로 보기 편하고, 컴파일 단계에서 발생할 수 있는 예외를 전부 핸들링 할 수 있다고 생각해서 RuntimeException보다 Exception을 사용했습니다!!

이 코드를 작성했던 팀원이 남긴 답변이다.

의도는 좋지만 체크 예외의 목적과는 약간 동떨어진 사용으로 보이는데요, 체크 예외는 예외 발생 시 복구를 목적으로 예외처리를 강제하는 느낌이라서요
실제 업무에서도 런타임 예외만 주로 사용하는 편이긴 합니다. 잘 안쓰는 추세이다보니 코틀린에선 체크 예외를 거의 없다시피 취급하기도 하는데요, 이 글도 읽어보시면 좋을 것 같네요!

이에 대한 산토리의 답.

나도 실제로 체크 예외는 컴파일에러를 체크하도록 하는 불편함을 가중시킨다는 말을 들었었다. 복구를 하는 것이 사실상 불가능한 경우에도 컴파일 단계에서 예외처리를 강제하게 하기 때문이었던 걸로 기억한다.

산토리가 이에 대한 레퍼런스로 달아준 글이다

Java의 Checked Exception은 실수다?

자바에선 Checked Exception를 애플리케이션이 예상하고 복구해야 하는 예외조건으로 취급한다. 존재하지 않는 파일을 읽으라고 했을 때 예외가 발생하는데, 이는 복구가 필요한 예외라고 할 수 있다.

런타임 예외는 애플리케이션 내부적으로 예외적인 상황으로, 보통 이 예외를 예상하거나 복구할 수 없다. 그래서 복구 처리가 강제되지 않는다.

checked exception은 ‘실패’가 아닌 ‘우발적인 상황’을 처리하려는 시도였습니다. 예측가능한 예외를 강조하고 개발자가 이를 처리하게 하는 것이었습니다.

하지만 광범위한 시스템과 복구 불가능한 실패를 강제로 선언하는 것에 대해서는 생각을 하지 못했습니다. 이러한 실패는 checked exception으로 선언될 수 없었습니다.

view에 의존하는 엔티티

    public ResponseMember toResponse() {
        return new ResponseMember(memberId, nickname, birthday, joinTime, email);
    }
    

산토리는 엔티티 내부에 response dto, 즉 뷰를 위한 dto에 의존성이 있는 것을 주의하라고 했다.

클라이언트 단에서 원하는 데이터 형식이 바뀌면 엔티티에 변화가 생겨야 하기 때문이다.

dto를 만드는 이유는 요구사항이 바뀌더라도 엔티티는 놔두고 dto만 변경해서 요구사항에 대응하기 위해서라고 한다.

dto에 엔티티가 의존적이 된다면 dto를 만드는 목적 자체에 위배되는 일이 되는 게 아닐까?하는 생각이 든다.


@Entity
@Getter
@Setter
public class Reservation {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version()
    private long version;
    @Column(nullable = false)
    private LocalDateTime registeredAt;
    @Column(nullable = false)
    private int amount;
    @Column(nullable = false)
    private int personCount;
    @Column(nullable = false)
    private LocalDate checkIn;
    @Column(nullable = false)
    private LocalDate checkOut;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "accommodation_id", nullable = false)
    private Accommodation accommodation;

    public ReservationResponse toEntity() {
        ReservationResponse reservationResponse = new ReservationResponse();
        reservationResponse.setUser(user.toEntity());
        reservationResponse.setAccommodationId(AccommodationMapper.toResponse(accommodation));
        reservationResponse.setRegisteredAt(registeredAt);
        reservationResponse.setAmount(amount);
        reservationResponse.setPersonCount(personCount);
        reservationResponse.setCheckIn(checkIn);
        reservationResponse.setCheckOut(checkOut);

        return reservationResponse;
    }

이번에 air-dnb를 만들면서 이런 코드를 작성했었다. view가 아니라 json 형태로 내보내는 방식이지만 산토리의 코멘트가 여기에도 적용될 거 같다.

Layered Pattern

모든 계층은 자기보다 아래에 있는 하위 계층에 의존해야 한다는 말이 있다.

Presentation layer는 business layer에게 의존하고, business layer는 persistence layer에게만 의존한다.
Presentation layer에서 Business layer를 건너뛰고 Persistence layer에 접근하는 일은 절대 일어나지 않는다.

엔티티가 web계층에 속하는 dto를 직접 의존하는 일은 피해야할듯하다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글