본문 바로가기

BackEnd

[Spring] 도메인과 서비스 리팩토링

Part 1. 도메인

  • setter 어노테이션 삭제
    • 캡슐화 위반
    • 객체 지향 특징 중 하나가 캡슐화이다. 캡슐화란 정보 은닉과 유사한 개념인데 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 외부로부터 보호하는 것을 말한다. 그러나 setter는 외부에서 private한 객체의 내부 상태를 직접 변경할 수 있도록 허용하는 방식으로 캡슐화 원칙에 위배된다
    • 사용자 의도를 쉽게 파악하기 어려움 (의도가 코드에 드러나지 않음)
    • 왜 해당 값으로 설정하는지 set 사용부분만 봐서는 알기 어렵다
    위와 같은 이유로 setter를 지양한다. 이전 코드에서는 setter를 dto 변환 과정에서만 사용하여 build로 코드를 바꿔주었다
  • 연관된 도메인 객체(User, Device 등)를 엔티티 객체로 참조하도록 (단방향 매핑)

💡

Spring JPA 기반의 도메인 설계에서는 연관된 도메인을 엔티티 객체로 필드에 선언하는 것이 맞다. ID만 넣는 방식은 DTO나 단순 데이터 전달에 적합하며 도메인(엔티티)에서는 객체 참조를 통한 관계 매핑이 표준적이다

 

수정 전 코드는 다음과 같이 필드에 id로만 선언을 했다. 하지만 테이블은 외래키를 통해 연관관계를 맺지만 엔티티(객체)는 참조를 통해 연관관계를 맺기 때문에 객체 참조로 수정하였다

JPA에서 연관관계 매핑을 위해 @ManyToOne@OneToMany@OneToOne@ManyToMany 어노테이션을 제공한다. 이를 사용하여 단방향 매핑 관계를 맺도록 하였다.

package teamplace.pixi.shop.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import teamplace.pixi.Device.domain.Device;
import teamplace.pixi.user.domain.User;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "review")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ShopReview {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long reviewId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="shop_id")
    private Shop shop;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="device_id")
    private Device device;

    private Integer reviewStar;

    private String reviewTitle;

    @Column(length = 1000)
    private String reviewContent;

    private Integer reviewMoney;

    private String reviewTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @Column(name = "review_created_at")
    private LocalDateTime createdAt;

}

 

 

Part 2. 서비스

다시 봐도 머리가 어질어질해지는 코드인것같다

  • DTO 변환 메서드는 DTO 내부에 선언
    service에서 dto 변환 메서드 선언 + setter 사용 ⇒ dto 내부에 변환 메서드 선언 & service에서 호출 +
    빌더로 새로운 dto 객체 생성으로 바꿔 주었다.

    왜???? 현재 단순 변환 로직밖에 없기 때문에 Like,,,
    현재 코드로서는 Response/Request DTO가 따로 있어 변화 로직이 복잡하지 않고 일관된 변환을 보장하기 위해 DTO 내부에서 정의하여 서비스 계층을 간단하게 수정하였다. 


  • ShopService → DeviceRepository 직접 호출 ⇒ ShopService → DeviceService → DeviceRepository
    https://velog.io/@lej7122/Spring-한-Service가-다른-Service-또는-Repository를-의존하는-경우
    위 블로그를 참고하여 순환참조가 일어나지 않는 것을 확인하고 Device Service를 호출하였다. 단순히 device Repository만 사용한다면 레포지토리를 선언해도 되지만 비지니스 로직을 포함하기 때문에 Device Service를 호출하는 것으로 선택했다.

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import teamplace.pixi.Device.domain.Device;
import teamplace.pixi.Device.dto.PartListViewResponse;
import teamplace.pixi.Device.repository.DeviceRepository;

import java.util.List;

@RequiredArgsConstructor
@Service
public class DeviceService {
    private final DeviceRepository deviceRepository;

    public List<Device> searchDevicesByName(String keyword) {
        return deviceRepository.findByNameLike(keyword);
    }

    public PartListViewResponse getPartListView(Long deviceId) {
        Device device = deviceRepository.findById(deviceId)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 기기입니다."));
        return new PartListViewResponse(device);
    }

    public Device getDeviceById(Long deviceId) {
        return deviceRepository.findById(deviceId).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 기기입니다."));
    }

    public List<String> getDeviceCategory(Long deviceId){
        Device device = deviceRepository.findById(deviceId)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 기기입니다."));

        String device_maker = device.getDeviceBrand();
        Integer deviceTypeCode = device.getDeviceType();  // ex: 0, 1, 2, 3

        String deviceType;

        switch (deviceTypeCode) {
            case 0 -> deviceType = "핸드폰";
            case 1 -> deviceType = "노트북";
            case 2 -> deviceType = "패드";
            case 3 -> deviceType = "악세사리";
            default -> throw new IllegalArgumentException("알 수 없는 기기 타입입니다.");
        }

        return List.of(device_maker, deviceType);

    }
}

 

내 코드가 이렇게 되는걸 방지하기 위해 꾸준히,.. 노력할것이다. 산학 팀 FIXI 파이팅