[내일배움캠프 본캠프]6일 차 TIL RestTemplate, Entity 연관 관계

2026. 2. 16. 03:46·내배캠

6일 차 TIL

Java 단기 심화 기간 동안에는 주말과 연휴에도 빠짐없이 Til을 작성해보려 한다.

Til을 작성하다 보면 자연스럽게
오늘은 물론이고
어제 적은 Til도 복습하게 되고
내일 뭘 공부할 지에 대해서도 생각하게 된다.

 

엄청난 선 순환? 사실 Til에는 어제, 오늘, 내일이 다 묶여서 움직이는 느낌이다.

 

오늘은 Spring 숙련강의 2강 중 RestTemplate, Naver Open API (외부 API 사용법),1대1 부터 N 대 M 관계 등 Entity 연관 관계에 대해서 배웠다.

RestTemplate이란?

서버와 서버간의 요청을 처리하는 API로써, Spring에서는 RestTemplate 라이브러리를 통해 이 기능을 지원하고 있다.

쉽게 말해서, 외부 API를 사용한다는 말은 내 서버(로컬, 개발 서버 등)에서 다른 서버가 미리 만들어 놓은 컨트롤러로 요청을 보내는 것이다.

 

RestTemplate을 사용하려면 먼저 의존성을 주입받아야 한다.
이때 생성자 파라미터로 주입받으면 되는데, RestTemplate은 그 객체로 바로 주입받을 순 없고,
RestTemplateBuilder 객체의 build() 메서드로 RestTemplate 객체를 반환받을 수 있다.

RestTemplate의 외부 서버로 요청 보내는 메서드

  1. GET요청 시 : getForEntity(요청 uri, 응답받을 클래스 타입, (선택) 경로 변수)
  2. POST요청 시 : postForEntity(요청 uri, 요청 body에 들어갈 객체, 응답받을 클래스 타입)

GET요청 시에 선택적으로 경로변수를 파라미터로 넣을 수 있다.
ex) 외부서버 엔드포인트가 "/api/server/post-call/{query}" 일 때, getForEntity(uri, ItemDto.class, 1)
이렇게 {query}를 1로 지정해 줄 수 있다.

 

uri 예시 1

URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/get-call-obj")
            .queryParam("query", query)
            .encode()
            .build()
            .toUri();

쿼리 스트링 방식으로 보낼 때. queryParam() 사용

 

uri 예시 2

URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/post-call/{query}")
            .encode()
            .build()
            .expand(query)
            .toUri();
    log.info("uri = " + uri);

PathVariable 방식으로 보낼 때. expand() 사용

 

ResponseEntity <>로 응답받은 데이터 저장

 ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);
 return responseEntity.getBody();

전달받은 값은 ResponseEntity에 담고, getBody() 메서드로 사용한다.

ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return fromJSONtoItems(responseEntity.getBody());

    public List<ItemDto> fromJSONtoItems(String responseEntity) {
        JSONObject jsonObject = new JSONObject(responseEntity);
        JSONArray items  = jsonObject.getJSONArray("items");
        List<ItemDto> itemDtoList = new ArrayList<>();

        for (Object item : items) {
            ItemDto itemDto = new ItemDto((JSONObject) item);
            itemDtoList.add(itemDto);
        }

        return itemDtoList;
    }

여러 개의 데이터를 받을 때는 ResponseEntity의 제네릭 타입으로 String으로 받는다.
그 후 JSON데이터를 String으로 받은 후 직접 저장할 DTO로 변환해 준다.

 

String => Json 객체 변환 라이브러리

// json
implementation 'org.json:json:20230227'

 

String을 JSON 객체로 변환할 때는 JSONObject 객체를 사용한다.

 


RestTemplate의 exchange() 메서드
- 헤더에 요청을 포함하고 싶을 때 주로 사용하는 메서드이다.

getForEntity나 postForEntity처럼 HTTP 메서드를 지정한 메서드가 아니라
exchange() 메서드를 사용하면 url, HTTPMethod, header, body, 응답을 받을 클래스를 지정해서 보낼 수 있다.

public List<ItemDto> exchangeCall(String token) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/exchange-call")
            .encode()
            .build()
            .toUri();
    log.info("uri = " + uri);

    User user = new User("Robbie", "1234");

    RequestEntity<User> requestEntity = RequestEntity
            .post(uri)
            .header("X-Authorization", token)
            .body(user);

    ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

    return fromJSONtoItems(responseEntity.getBody());
}

Naver Open API

- RestTemplate을 활용해서 Naver Open API의 검색기능 이용하기
외부 API는 요청 시 해당 업체의 api키를 발급받아서 사용해야 한다.
네이버의 경우 Client Id + Client Secret을 받아서 헤더에 포함시켜서 요청을 보내야 했다.

출처 : https://developers.naver.com/products/intro/plan/plan.md

RestTemplate + Naver 검색 API

원래 RestTemplate의 개발 흐름은 다음과 같다.
1. 요청 URI를 작성한다.
2. 외부 서버로 요청을 보낸다.
3. 응답 데이터를 저장한다.
4. 반환한다.

이때 2번에서 restTemplate.exchange() 같은 메서드를 사용하는 것이고,
3번에서 JsonObject 객체로 저장하여 커스텀 DTO 가공한다.

여기에 추가로 Naver 검색 API 사용을 위해
1. API 키를 발급받는다.
2. 요청 헤더에 키값을 포함시킨다.
위 2개의 과정이 포함된다.

검색의 경우에는 쿼리스트링으로 검색어를 입력받아야 하기에
컨트롤러에서 @RequestParam으로 검색어를 전달받았다.

서비스에서는 검색결과로 여러 개의 데이터를 전달받기 때문에
String으로 Json 객체를 저장하고
JSONObject의 getJSONArray() 메서드를 사용한다.

받은 JSON 중 다시 원하는 필드만 받기 위해
for문을 통해서 배열의 각 인덱스를 커스텀한 Dto 객체로 저장한다.

참고로 서버측면에서의 서버에서 ResponseDto에서 List <>를 반환할 때
그 필드명으로 JSON 형태가 나간다.
즉 아래와 같이 items 가 필드명이라면?

package com.sparta.springresttemplateserver.dto;

import com.sparta.springresttemplateserver.entity.Item;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

@Getter
public class ItemResponseDto {
    private final List<Item> items = new ArrayList<>();

    public void setItems(Item item) {
        items.add(item);
    }
}

그리고 아래와 같이 List 반환 메서드가 작성되어 있다면?

public ItemResponseDto getCallList() {
    ItemResponseDto responseDto = new ItemResponseDto();
    for (Item item : itemList) {
        responseDto.setItems(item);
    }
    return responseDto;
}

이때 return 되는 responseDto는 객체의 필드명을 Key값으로 가진 배열 형태가 된다.

"items" : 
[
    {
    ...
    },
    {
    ...
    },
    ...
]

 

Entity 연관 관계

DB의 테이블 간의 관계를 표현할 때 4가지로 표현한다.

  1. 1 대 1
  2. 1 대 N
  3. N 대 1
  4. N 대 M

고객과 음식이 있을 때

고객 1명이 N개의 음식을 주문할 수 있고
음식 1개는 고객 N명에게 주문될 수 있을 때
고객과 음식은 N 대 M의 관계이다.


이때 N 대 M에 관계에서는 중간 테이블로써 주문 테이블을 만들어 관리할 수 있다.

DB 간의 방향

DB에서 방향은 단방향과 양방향으로 나눈다.
단방향은 한쪽만 상대 테이블을 조회할 수 있음
양방향은 서로가 상대 테이블을 조회할 수 있음


SQL에서는 어떤 테이블을 기준으로 하든 조회 가능 ( DB 테이블 간의 관계에서는 방향 개념이 없다. )
JPA에서는 기준 테이블에 따라 상대를 조회할 수도 있고 못 할 수도 있다. ( 단방향/양방향 모두 존재 )

외래키의 주인은 일반적으로 N(다)의 관계인 Entity다. ( 1 대 1의 관계에서는 주인을 직접 지정해야 함. )
@JoinColumn을 통해 외래키의 주인을 지정할 수 있다.

단방향의 경우엔 외래키의 주인 쪽만 상대 Entity 타입의 필드를 가지면서 @JoinColumn의 name 옵션에
양방향의 경우엔 추가로 상대 Entity에서 외래키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션으로
@JoinColumn 이 설정된 필드명을 넣어야 한다.

@JoinColumn의 name 속성은 상대 엔티티 필드명 + pk컬럼명이다.

예시) Order에서 User를 참조할 때, User의 pk 컬럼명이 id 라면?
@ManyToOne
@JoinColumn(name="user_id")
private User user;

1 대 다 설명을 추가)
@JoinColumn의 name 속성은 외래키가 실제로 생성될 테이블(N 쪽)에 만들어질 컬럼명이다.

 

그리고 1대 1이나 다대 1 경우엔 객체 그대로 받아오면 되지만

1대다의 경우엔 List <>로 받아와야 한다.

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>();
}

연관 관계를 나타낼 때,
지금 사용하고 있는 클래스를 앞에 쓰고
참조할 클래스를 뒤에 쓴다.


위 코드의 경우엔
1에 해당하는 것이 User이고
N(다)에 해당하는 것이 Food이다.
그리고 외래키의 주인은 Food이다.

1 대 다의 관계

보통 외래키는 항상 N(다)의 관계에서 생성되지만,
1 대 다의 관계에서 @JoinColumn을 통해 1의 관계가 외래키의 주인으로 지정해 줄 수 있다.
이때 외래키의 소유는 1이 가지지만, 실제 외래키는 다의 관계에 저장된다.

일반적으로 1 대 다의 관계에서는 양방향 관계가 존재하지 않지만
@JoinColumn의 속성값을 추가해 억지로 설정할 순 있다.
다의 관계에서 @JoinColumn으로 상대 필드를 가져올 때
속성값으로 insertable = false, updatable = false를 추가한다.


하지만 이때, 외래키를 1의 관계에서 소유하지만 테이블은 다의 관계에 있기 때문에,
insert시 추가적인 update 쿼리가 발생하는 한계점이 있다.

1의 관계를 가지는 Food 엔티티

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany
    @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
    private List<User> userList = new ArrayList<>();
}

나는 위 코드를 "1의 관계에서 @JoinColumn을 통해 실제 외래키가 생성될 곳에서 할 외래키 설정을 대신할 수 있다"로 이해했다.

 

다의 관계를 가지는 User 엔티티

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

1대다의 관계에서는 양방향이 없으므로
단방향으로 구성했다.

 

1 대 다에서의 양방향 설정을 가지는 User 엔티티

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

		@ManyToOne
		@JoinColumn(name = "food_id", insertable = false, updatable = false)
		private Food food;
}

다의 관계에서 억지로 양방향 설정을 한 모습이다.

N 대 M의 관계

N 대 M의 관계에서 생성되는 중간 테이블을 JPA로 자동 생성할 수 있다.
이때는 N, M 둘 다 외래키를 가지지 않고
중간 테이블에서 외래키를 모두 소유한다.

 

N 대 M에서의 N의 관계인 Food 엔티티

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
    joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
    inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();
}

joinColumns 옵션으로 현재 위치인 Food entity의 조인 컬럼설정
inverseJoinColumnsd 옵션으로 상대 위치인 User Entity의 조인 컬럼 설정

위 2개의 옵션으로 중간 테이블에 외래키를 설정할 수 있다.

양방향의 경우엔 상대 엔티티에서 @ManyToMany(mappedBy = "userList")를 설정하면 된다.

하지만 중간 테이블이 자동 생성되면 조작하기 어렵기 때문에
직접 생성할 수도 있다.

중간 테이블

다 대 다에서, 중간테이블을 설정한다면
조인할 각 테이블과 중간테이블과의 관계는 1 대 다가 된다.

'내배캠' 카테고리의 다른 글

[내일배움캠프 본캠프] 8일 차 TIL 페이징처리, 다대다, AWS  (0) 2026.02.17
[내일배움캠프 본캠프] 7일 차 TIL 지연로딩, cascade, scheduler, 헤더 JWT  (0) 2026.02.16
[내일배움캠프 본캠프] 5일 차 TIL Spring Security + JWT  (0) 2026.02.14
[내일배움캠프 본캠프] 4일 차 TIL Bean 수동 등록, 쿠키/세션  (0) 2026.02.14
[내일배움캠프 본캠프] 3일 차 TIL 영속성 컨텍스트  (0) 2026.02.12
'내배캠' 카테고리의 다른 글
  • [내일배움캠프 본캠프] 8일 차 TIL 페이징처리, 다대다, AWS
  • [내일배움캠프 본캠프] 7일 차 TIL 지연로딩, cascade, scheduler, 헤더 JWT
  • [내일배움캠프 본캠프] 5일 차 TIL Spring Security + JWT
  • [내일배움캠프 본캠프] 4일 차 TIL Bean 수동 등록, 쿠키/세션
MvA
MvA
백엔드 개발자 김재현입니다. 주로 공부하면서 느낀점을 기록합니다.
  • MvA
    Man vs Ai
    MvA
  • 전체
    오늘
    어제
    • 분류 전체보기 (94)
      • Java (6)
      • Python (8)
        • 딥러닝 (1)
        • 머신러닝 (7)
      • JavaScript (2)
      • 내배캠 (60)
      • 개인 프로젝트 (11)
      • 책 후기 (5)
      • 기타 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    TiL
    배포
    내일배움캠프
    Riot API
    머신러닝
    아키텍처
    딥러닝
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
MvA
[내일배움캠프 본캠프]6일 차 TIL RestTemplate, Entity 연관 관계
상단으로

티스토리툴바