Post

[대외활동] 멋쟁이사자처럼·백엔드 크로스오버 과제 API 설계 고민기록 (Spring Boot)

[대외활동] 멋쟁이사자처럼·백엔드 크로스오버 과제 API 설계 고민기록 (Spring Boot)

멋쟁이사자처럼(대학) 11주차 크로스오버 과제

간단한 과제였지만 구조와 흐름, 코드 설계를 어떻게 리팩토링 해야할 것인가에 집중했다.

프론트와 협업해볼 프로젝트- 감정일기
https://emotion-diary.winterlood.com/

백엔드 API 구현 고려사항

  • 월별 일기 목록 조회
  • 단일 일기 상세 조회
  • 일기 생성
  • 일기 수정
  • 일기 삭제

☑️ 고민1. toEntity() / toDto() 위치와 필요성

✅ “toEntity(), toDto() 내부에 localDateTime → timestamp(ms) 변환 로직 넣을 것인가?” ↔️ “Service 내부에 구현할 것인가?”

프론트는 timestamp(epoch time) 의 표현방식으로 api를 요청하는 것이 가볍지만, 백엔드 쪽에서는 엔지니어가 보기 편하고 날짜를 계산하기 좋은 LocalDateTime 을 사용하게 된다.

해당 변환 로직을 Service 에 구현할 것인지, Dto 의 매핑 메서드 (toEntity()) 에 구현할 것인지에 대한 고민이었다.

우선 toEntity() 내부에 변환 로직을 넣기도 한 이유는

  1. 책임을 한 곳에 묶어서 처리하고자 함.
  2. Service 내부 코드를 깔끔하게 작성하기 위함

변환 로직이 필요한 곳마다 구현하는 것보다 DTO 의 toEntity 내부에 구현함으로서 DB 에 넣을 수 있는 Entity 객체를 만들고자 하였다.

1
2
Diary diary = diaryRequest.toEntity();
diaryRepository.save(diary);

☑️ 고민2. Entity → DiaryResponse 변환 방식

** ✅ Entity → DTO 변환을 어떻게 할까? **

① 생성자 직접 호출 방식

1
2
3
4
5
6
return new DiaryResponse(
    diary.getId(),
    convertToMillis(diary.getCreatedDate()),
    diary.getEmotionId(),
    diary.getContent()
);

생성자를 직접 호출하는 방식은 간단하고 한눈에 볼 수 있다는 장점이 있었지만, 비즈니스 로직을 구현하는 Service 에서 응답 DTO 을 변환하는 것까지 구현한다는게 불필요 하다고 생각하였다.

DiaryResponse 에 새로운 필드를 추가할 시, 전부 수저앻야 한다는 점이 큰 단점으로 작용하였다.

② Entity 안에 toDto() 넣기

1
2
3
4
5
6
7
public DiaryResponse toDto() {
    return new DiaryResponse(
        id,
        createdDate.atZone(ZoneId.of("Asia/Seoul")).toInstant().toEpochMilli(),
        content
    );
}

diary.toDto() 로 호출하는 것이 가장 편리해보였고, toDto 란 메서드명 역시 그 의미가 직접적으로 와닿았다.

하지만 Entity 클래스는 데이터베이스 테이블과 직접적으로 매핑되는 클래스로서 주된 목적이 DB 와 데이터를 주고 받는 것이므로,

클라이언트와 응답용으로 설계된 DTO 를 알고 있는다는 것이 책임분리 원칙에 어긋난다고 생각하였다고, 계층 간 불필요한 의존성을 초래할 것이라 생각하였다.

③ ResponseDTO 내부에 fromEntity() 만들기

1
2
3
4
5
6
7
8
9
// DiaryResponse.java
public static DiaryResponse fromEntity(Diary diary) {
    return new DiaryResponse(
        diary.getId(),
        convertToMillis(diary.getCreatedDate()),
        diary.getEmotionId(),
        diary.getContent()
    );
}
1
2
3
4
    private static Long convertToMillis(LocalDateTime dataTime) {
        Instant instant = dataTime.atZone(ZoneId.of("Asia/Seoul")).toInstant();
        return instant.toEpochMilli();
    }

DTO 가 응답 구조를 책임진다는 것이 관심사가 가장 명확해보였고, Service 는 단순히 fromEntity() 만 호출하면 되므로 가장 효율적인 구조라 생각하였다.

☑️ 고민3. Entity 업데이트 방식

✅ Service에서 Entity의 필드를 수정해야 할 때 어떤 방식으로 할까?

① Setter 직접 사용

1
2
diary.setContent(dto.getContent());
diary.setEmotionId(dto.getEmotionId());

가장 직관적이고 간단하지만, 유지보수 시 수정해야 할 곳이 여러 군데 생기기 쉬울 것이라 생각하였다.

② Entity 내부에 updateFrom(DTO) 메서드 작성

1
2
3
diary.setContent(dto.getContent());
diary.setEmotionId(dto.getEmotionId());
diary.setCreatedDate(dto.toLocalDateTime());

해당 고민 역시 Entity가 DTO를 알게 된다는 게 가장 큰 단점으로 작용하였고, 계층 분리를 위반하므로 적절치 못한 생각이였다.

③ Request DTO 내부에 updateEntity(Entity) 메서드 작성

1
2
3
4
5
public void updateEntity(Diary diary) {
    diary.setContent(this.content);
    diary.setEmotionId(this.emotionId);
    diary.setCreatedDate(this.toLocalDateTime());
}
1
2
3
4
5
6
7
8
9
10
11
// DiaryService.java
    public DiaryResponse updateDiary(Long id, DiaryRequest diaryRequest) {
        Optional<Diary> optional = diaryRepository.findById(id);
        if (optional.isPresent()) {
            Diary diary = optional.get();
            diaryRequest.updateEntity(diary);
            diaryRepository.save(diary);
            return DiaryResponse.fromEntity(diary);
        }
        return new DiaryResponse(); // 예외처리 (없음)
    }

Entity는 DB 구조를 책임지는 모델 객체로만 사용하고 싶은 목적이 있었고, 수정 로직은 한 군데에 몰아둘 수 있어서 응집도가 높으며 재사용도 쉽다는 장점이 있었다.

☑️ 고민4. 계층에 따른 메서드 네이밍

Service 계층에서 findBy~ 네이밍을 주로 사용했는데,
이는 사실 Repository에 적합한 네이밍이기 때문에
Service에서는 get~이나 목적 중심의 네이밍으로 구분할 필요를 느꼈다.
그래서 각 계층의 역할에 맞게 메서드 네이밍을 정리하고 통일하고자 한다.

This post is licensed under CC BY 4.0 by the author.