[JPA] 연관 관계를 가진 엔티티의 생성
# Back-End/Spring

[JPA] 연관 관계를 가진 엔티티의 생성

다대일의 관계에서 데이터를 저장할 때 연관관계를 매핑시키는 방법에 대한 글입니다.

 

문제상황

"유저는 여러 개의 상점을 등록할 수 있다."

상점을 저장하기 위해 유저에 대한 정보를 갖게 해줘야 했습니다.

(비슷한 경우는 게시물 저장 시 사용자의 정보 갖게 하기)

 


 

먼저 저장에 사용되는 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가지입니다.

  1. DTO의 연관관계를 어디서 갖게 해야할까 (클라이언트단, Controller단, Service단)
  2. 클라이언트는 User 정보를 갖고있어도 될까

클라이언트에서 User라는 민감한 정보를 갖고있으면 안될 것 같고, Controller에서 엔티티에 직접 접근하거나 생성하는 일이 없도록 생각하니 서비스단에서 UserService를 호출하고 DTO에 채워줌으로 Service단에서 처리하였다.

 

 

최종 Data Flow

  1. 클라이언트에서 서버로 저장 요청 (이때 서버가 받은 SaveRequestDto는 사용자 정보가 없는 상태, 저장할 값만을 담고있음)
  2. Controller는 Service를 호출하여 StoreSaveRequestDto와 UserRequestDto를 넘겨줌 (Session에서 유저 정보를 불러온 것)
  3. Service에서 UserRequestDto를 이용하여 UserRepository에서 User 엔티티를 가져와서 StoreSaveRequestDto에 값을 채워줌
  4. 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