코드스쿼드 8 - 도메인 Service, put/patch

Alex·2024년 7월 4일
0

리팩토링

목록 보기
9/17

한 도메인의 서비스에서 이렇게 다른 도메인들의 DB에 접근해야 하는 일이 있을 때 어떻게 처리하는 것이 좋을지 항상 고민하게 되는 것 같습니다.
지금처럼 다른 도메인의 레포지토리를 바로 참조하는 방식 → 다른 도메인의 레포지토리를 모두 가지고 있게 되면서 서비스가 의존하고 있는 요소들이 많아지고 도메인을 넘나들게 되는 것이 꺼림직합니다.
다른 도메인의 서비스 계층을 통해 레포지토리를 접근하는 방식 → 서비스가 서로 상호 참조하는 상황이 가능해지는 문제가 있습니다.
이번에는 내용이 많이 없고, 상호 참조 문제가 좀 더 피해야 하는 문제라고 생각되어서 첫번째 방법을 선택하게 되었는데 이런 문제로 고민할 때 더 고려해야 하는 상황이라던가 더 적합한 방법이 있을까요?


public class AccommodationService {

    private final AmazonS3Client amazonS3Client;
    private final AccommodationRepository accommodationRepository;
    private final PictureRepository pictureRepository;
    private final HashtagRepository hashtagRepository;
    private final AccommodationHashtagRepository accommodationHashtagRepository;
    private final FilterManger filterManger;

말씀해주신대로 특정 도메인의 서비스가 다른 도메인의 영속성 로직을 들고있는건 장기적으로 봤을 때 좋지 못한 그림이라 생각합니다. 서비스가 서로 상호 참조하는 상황을 고려해서 제거하려고 하신 부분도 너무 좋구요!
조금 더 추가로 말씀드리고 싶은 부분은, CommenService 입장에서는 User 정보만 필요할 뿐, 그 이상의 정보는 불필요합니다. User와 같은 타 도메인 조회로직이 복잡해지면 Comment와 관련된 비즈니스로직의 가독성이 떨어지는 문제도 있고, 타 도메인의 데이터를 수정하고 삭제할 수 있는 전권을 가진 무적 서비스가 될 위험도 있어요.
저의 경우, Repository를 한 번 더 감싸고 조회와 관련된 로직만 호출할 수 있도록 Repository를 래핑한 조회전용 클래스를 따로 만들어 사용합니다. (이후 서비스 규모가 커지고 아키텍쳐 복잡도가 높아지면 R와 CUD를 분리해서 관리해야하는 상황이 올 수도 있습니다) 그렇게 되면 일단 CommentService가 타 도메인의 영속 레이어를 제한적으로 사용할 수 있도록 만들어줍니다.
그리고 하나의 서비스에서 너무 많은 도메인들이 관여하고 있다면, 서비스들을 모아주는 한 층 더 상위의 레이어를 두어도 좋다고 생각합니다. controller - service - repository 계층은 기본으로 가져가되 상황에 맞게 좀 더 디벨롭해주는 형태도 고민해볼 수 있어요.
같은 서비스에 두는 것이 맞는지에 대한 고민을 할 때 도메인간의 관계도 함께 생각해보면 좋겠습니다.
도메인 간의 관계에서도 나름의 계층이 존재하고 포함관계로 나타낼 수 있는 부분들이 있습니다

예를 들어 주문 도메인은 내부에 주문, 품목, 배송정보 등 주문이라는 도메인을 구성하기 위해 반드시 필요한 하위 도메인들이 있습니다. 그렇게 되면 주문 서비스 내부에서는 하위 도메인을 다루는 서비스가 반드시 들어가는게 좋겠죠. 그 이외의 도메인이면서도 완전 독립적으로 분리가 가능한 도메인이라는 판단이 든다면 완전 다른 도메인 서비스로 분리하여 서비스들 간 통합시켜줄 수 있는 클래스에서 관리할 수도 있습니다
(여기서 또 회사에서 배송 서비스가 매우 크고 중요할 경우 별도로 빼줄 수도 있겠죠 - 상황에 따라 유연하게 묶이는 영역이 달라질 수 있음)

1) service를 읽기전용과 crud 전용으로 나누는 것
이렇게 되면 제한적으로만 권한을 줄 수 있다.

2)서비스들을 모아주는 하나의 레이어를 만드는 것

이 두가지를 사용할 수 있는 것 같다.

2번 같은 방식은 감이 잘 안잡힌다.
gpt에게 물어보니

public class AccommodationService {
    private final AccommodationRepository accommodationRepository;
    private final AccommodationAggregator accommodationAggregator;

    public AccommodationService(AccommodationRepository accommodationRepository, AccommodationAggregator accommodationAggregator) {
        this.accommodationRepository = accommodationRepository;
        this.accommodationAggregator = accommodationAggregator;
    }

    public void registerAccommodation(Accommodation accommodation, List<String> pictureUrls, List<String> hashtags) {
        accommodationRepository.save(accommodation);
        accommodationAggregator.addPictures(accommodation.getId(), pictureUrls);
        accommodationAggregator.addHashtags(accommodation.getId(), hashtags);
    }

    // other methods...
}

// AccommodationAggregator.java
public class AccommodationAggregator {
    private final PictureService pictureService;
    private final HashtagService hashtagService;

    public AccommodationAggregator(PictureService pictureService, HashtagService hashtagService) {
        this.pictureService = pictureService;
        this.hashtagService = hashtagService;
    }

    public void addPictures(Long accommodationId, List<String> pictureUrls) {
        pictureService.savePictures(accommodationId, pictureUrls);
    }

    public void addHashtags(Long accommodationId, List<String> hashtags) {
        hashtagService.saveHashtags(accommodationId, hashtags);
    }

    // other aggregate methods...
}

// PictureService.java
public class PictureService {
    private final PictureRepository pictureRepository;

    public PictureService(PictureRepository pictureRepository) {
        this.pictureRepository = pictureRepository;
    }

    public void savePictures(Long accommodationId, List<String> pictureUrls) {
        // logic to save pictures
    }

    // other methods...
}

// HashtagService.java
public class HashtagService {
    private final HashtagRepository hashtagRepository;

    public HashtagService(HashtagRepository hashtagRepository) {
        this.hashtagRepository = hashtagRepository;
    }

    public void saveHashtags(Long accommodationId, List<String> hashtags) {
        // logic to save hashtags
    }

    // other methods...
}

이런 방식을 쓸 수 있는 것 같다.

put vs patch

이 두가지 메서드는 사실 아직 감이 잘 안잡힌다.

어떤 상황에서 어떤 메서드를 선택해야 하는지는 속한 조직에서 정해진 규칙들을 따르는게 베스트고 그런 규약이 따로 없을 경우에는 세간에서 말하는 Restful 조건을 따르는게 좋다고 생각합니다.
개인적으로 API의 HTTP 메소드를 고민하는 가장 핵심적인 이유는 결국 클라이언트가 서버에게 데이터를 주면서 어떤 동작을 요청할지를 HTTP 메소드로 파악하기 때문일 것이라고 생각합니다.
속해있는 개발 조직마다 그 정의는 조금씩 다를 수도 있구요. 예를 들어, GET, POST만 사용하는 조직이 있을 수도 있습니다.
때문에 핵심은 우리 비즈니스를 구체적으로 알지 못하는 사람들이라도 API를 보고서 대부분의 사람들이 일어나는 액션을 예측할 수 있는 일관된 API를 만든다라는 관점을 견지하면서 상황에 맞는 선택을 하는게 가장 최선이라고 생각합니다. 더불어 커뮤니케이션 비용이 줄고, 호환성이 좋은 API를 만들 수 있는 장점도 따라올거구요.

(1)
특정 값을 업데이트를 할 때 User 혹은 Accommodation 전체를 가져와서 넣어주는 작업 -> 조직내의 특별한 규칙이 없다면 구현하신대로 일부 값만 들고와서 업데이트하는 것이 좋을 것 같고, 이 행위는 PATCH 와 가장 유사해보입니다

PUT API는 모든 필드를 업데이트 시키기 때문에 request 클래스는 a, b, c 모두 정의
PATCH API는 c 필드만 업데이트 시키기 때문에 request 클래스는 c만 정의
PUT이다 -> 전체 필드를 다 null로 업데이트 시켜주어야 하는구나
PATCH이다 -> c 필드만 업데이트 시켜주어야 하는구나
만약 하나의 API로 어쩔 때는 a도 업데이트, b도 업데이트, c도 업데이트 만능으로 사용하려고 하신다면 추천드리지는 않습니다. 호출 시 API 내부에서 어떤 행동이 일어날지 예측하기 어려워지고 만약 deprecate 해야하는 순간이 찾아오면 너무 많은 컨텍스트에서 사용하고 있기 때문에 쉽게 제거했을 때 벌어지는 사이드 이펙트를 파악하기도 어렵고 유지보수도 힘들어집니다.

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

0개의 댓글