Part 1. 도메인
- setter 어노테이션 삭제
- 캡슐화 위반
- 객체 지향 특징 중 하나가 캡슐화이다. 캡슐화란 정보 은닉과 유사한 개념인데 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 외부로부터 보호하는 것을 말한다. 그러나 setter는 외부에서 private한 객체의 내부 상태를 직접 변경할 수 있도록 허용하는 방식으로 캡슐화 원칙에 위배된다
- 사용자 의도를 쉽게 파악하기 어려움 (의도가 코드에 드러나지 않음)
- 왜 해당 값으로 설정하는지 set 사용부분만 봐서는 알기 어렵다
- 연관된 도메인 객체(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 파이팅
'BackEnd' 카테고리의 다른 글
| [Spring] WebSocket 과 RabbitMQ를 활용하여 채팅, 알림 구현하기 (0) | 2025.05.31 |
|---|---|
| [Spring] 응애의 1:1 채팅 구현기(1) - 메시지 브로커를 선택하다 (1) | 2025.05.24 |