다대일의 관계에서 데이터를 저장할 때 연관관계를 매핑시키는 방법에 대한 글입니다.
문제상황
"유저는 여러 개의 상점을 등록할 수 있다."
상점을 저장하기 위해 유저에 대한 정보를 갖게 해줘야 했습니다.
(비슷한 경우는 게시물 저장 시 사용자의 정보 갖게 하기)
먼저 저장에 사용되는 Store 엔티티와 DTO를 알아보겠습니다.
👉 Store 엔티티
@Getter
@NoArgsConstructor
@Entity
public class Store extends BaseTimeEntity {
@Id
@Column(name = "STORE_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name = "STORE_NM")
private String name;
@ManyToOne
@JoinColumn(nullable = false, name = "USER_ID")
private User ownerUser;
@Builder
public Store(String name, String info, String tel, String status, User ownerUser) {
this.name = name;
this.info = info;
this.tel = tel;
this.status = status;
this.ownerUser = ownerUser;
}
}
👉 StoreSaveRequestDto (클라이언트에서 서버로 POST Request 요청 시 데이터를 담을 객체)
@Getter
@NoArgsConstructor
public class StoreSaveRequestDto {
private String name;
private User ownerUser;
@Builder
public StoreSaveRequestDto(String name, User ownerUser) {
this.name = name;
this.ownerUser = ownerUser;
}
public void setOwnerUser(User ownerUser){
this.ownerUser = ownerUser;
}
public Store toEntity() {
return Store.builder()
.name(name)
.ownerUser(ownerUser)
.build();
}
}
1. Store 엔티티는 User엔티티를 멤버로 갖게 되어있습니다.(@ManyToOne)
(DB 상으로는 User 테이블의 PK인 USER_ID를 외래키로 갖게 된다)
2. DTO에 유저 정보를 채워주기 위해 User에 대해서만 Setter 메소드를 따로 구현하였다.
고민한 것과 조치한 것
연관관계를 채워주기 위해 고민했던 방법이 2가지입니다.
- DTO의 연관관계를 어디서 갖게 해야할까 (클라이언트단, Controller단, Service단)
- 클라이언트는 User 정보를 갖고있어도 될까
클라이언트에서 User라는 민감한 정보를 갖고있으면 안될 것 같고, Controller에서 엔티티에 직접 접근하거나 생성하는 일이 없도록 생각하니 서비스단에서 UserService를 호출하고 DTO에 채워줌으로 Service단에서 처리하였다.
최종 Data Flow
- 클라이언트에서 서버로 저장 요청 (이때 서버가 받은 SaveRequestDto는 사용자 정보가 없는 상태, 저장할 값만을 담고있음)
- Controller는 Service를 호출하여 StoreSaveRequestDto와 UserRequestDto를 넘겨줌 (Session에서 유저 정보를 불러온 것)
- Service에서 UserRequestDto를 이용하여 UserRepository에서 User 엔티티를 가져와서 StoreSaveRequestDto에 값을 채워줌
- StoreSaveRequestDto를 Entity로 변환하고 저장
Controller단
@PostMapping("/stores")
public Long saveStore(@RequestBody StoreSaveRequestDto dto, @LoginUser SessionUser user){
UserRequestDto userDto = UserRequestDto.builder().user(user).build();
return storeService.save(dto, userDto);
}
Service단
@Transactional
public Long save(StoreSaveRequestDto requestDto, UserRequestDto userDto) {
User user = userRepository.findById(userDto.getId())
.orElseThrow(() -> new IllegalArgumentException("해당 유저는 없습니다. id = " + userDto.getId()));
requestDto.setOwnerUser(user);
return storeRepository.save(requestDto.toEntity()).getId();
}
단위 테스트
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class StoreApiControllerTest {
@LocalServerPort
private int port;
@Autowired
private StoreRepository storeRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
protected MockHttpSession mockHttpSession;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
mockHttpSession = new MockHttpSession();
createUser();
}
public void createUser(){
userRepository.save(User.builder()
.name("테스트")
.email("a@naver.com")
.picture("none")
.role(Role.USER)
.build()
);
}
@After
public void tearDown() throws Exception {
storeRepository.deleteAll();
userRepository.deleteAll();
}
@Test
@WithMockUser(roles = "USER")
public void 유저는_상점을_만든다() throws Exception {
//given
User testUser = userRepository.findAll().get(0);
SessionUser sessionUser = new SessionUser(testUser);
StoreSaveRequestDto dto = StoreSaveRequestDto.builder()
.name("상점")
.info("정보")
.tel("0101")
.status("영업")
//.ownerUser() // 클라이언트에서 User 빼고 전송하는 상황
.build();
//when
String url = "http://localhost:" + port + "/api/user/stores";
mockHttpSession.setAttribute("user", sessionUser);
//then
String dtoContent = new ObjectMapper()
.registerModule(new JavaTimeModule())
.writeValueAsString(dto);
mvc.perform(post(url)
.session(mockHttpSession)
.content(dtoContent)
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
}
}
728x90
'# Back-End > Spring' 카테고리의 다른 글
[Test] Spring Layer별 테스트 작성 (1) | 2022.01.23 |
---|---|
[Test] Spring Boot 테스트 클래스 정의 어노테이션 (0) | 2022.01.23 |
[Test] JUnit5를 이용한 테스트 코드 작성 (0) | 2022.01.23 |
H2 console 세팅 & 접속 (0) | 2021.11.19 |
[JPA] hibernate.ddl-auto 옵션 정리 (0) | 2021.10.11 |
인증과 인가란? (0) | 2021.06.21 |
ORM 과 SQL Mapper 비교 (0) | 2021.02.24 |