[TIL] 230521 <Spring> Bean, 인증과 인가, RestTemplate & Open API
- -
[Bean을 수동으로 등록하는 방법]
- @Component를 사용하면 @ComponentScan에 의해 자동으로 스캔되어 해당 클래스를 Bean으로 등록해줌
- 프로젝트의 규모가 커질 수록 등록할 Bean들이 많아지기 때문에 자동등록을 사용하면 편리
- 비즈니스 로직과 관련된 클래스들은 그 수가 많기 때문에 @Controller, @Service와 같은 애너테이션들을 사용해서 Bean으로 등록하고 관리하면 개발 생산성에 유리
- Bean 수동 등록은 언제 사용? >> 기술적인 문제나 공통적인 관심사를 처리할 때 사용하는 객체들을 수동으로 등록하는 것이 좋음!!!
- 공통 로그처리와 같은 비즈니스 로직을 지원하기 위한 부가 적이고 공통적인 기능들을 기술 지원 Bean이라 부르고 수동등록
- 비즈니스 로직 Bean 보다는 그 수가 적기 때문에 수동으로 등록하기 부담스럽지 않음
- 수동등록된 Bean에서 문제가 발생했을 때 해당 위치를 파악하기 쉬움
Config>PasswordConfig
(비밀번호를 암호화할 때 사용하는 PasswordEncoder의 구현체 BCryptPasswordEncoder를 Bean으로 수동등록)
package com.sparta.springauth.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class PasswordConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
- Bean으로 등록하고자하는 객체를 반환하는 메서드를 선언하고 @Bean을 설정
- Bean을 등록하는 메서드가 속한 해당 클래스에 @Configuration을 설정
- Spring 서버가 뜰 때 Spring IoC 컨테이너에 'Bean'으로 저장됨
- 'Bean' 이름: @Bean 이 설정된 메서드명
- public PasswordEncoder passwordEncoder() {..} → passwordEncoder
PasswordEncoderTest
(등록한 passwordEncoder ‘Bean’을 사용하여 문자열을 암호화)
package com.sparta.springauth; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.password.PasswordEncoder; @SpringBootTest public class PasswordEncoderTest { @Autowired PasswordEncoder passwordEncoder; @Test @DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화") void test1() { String password = "Robbie's password"; // 암호화 String encodePassword = passwordEncoder.encode(password); System.out.println("encodePassword = " + encodePassword); String inputPassword = "Robbie"; // 복호화를 통해 암호화된 비밀번호와 비교 boolean matches = passwordEncoder.matches(inputPassword, encodePassword); System.out.println("matches = " + matches); // 암호화할 때 사용된 값과 다른 문자열과 비교했기 때문에 false } }
[같은 타입의 Bean이 2개라면?]
같은 타입의 Bean 등록 (Chicken, Pizza)
BeanTest
- Food food; 필드에 @Autowired를 사용하여 Bean 객체를 주입하려고 시도하면 주입을 할 수 없다는 오류가 발생
ㄴ “Food 타입의 Bean 객체가 하나 이상 있습니다. “ - food 필드에 Bean을 주입해줘야하는데 같은 타입의 Bean 객체가 하나 이상이기 때문에 어떤 Bean을 등록해줘야할지 몰라 오류가 발생한 것!
해결 방법
1. 등록된 Bean이름 명시하기
BeanTest
package com.sparta.springauth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.sparta.springauth.food.Food; @SpringBootTest public class BeanTest { @Autowired Food pizza; @Autowired Food chicken; @Test @DisplayName("테스트") void test1() { pizza.eat(); chicken.eat(); } }
- 등록된 Bean 이름 pizza, chicken을 정확하게 명시
- @Autowired가 기본적으로는 Bean Type(Food)으로 DI를 지원하며,
연결이 되지않을 경우 Bean Name(pizza, chicken)으로 찾음
2. @Primary 사용하기
Chicken
BeanTest
package com.sparta.springauth; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.sparta.springauth.food.Food; @SpringBootTest public class BeanTest { @Autowired Food food; @Test @DisplayName("테스트") void test1() { food.eat(); } }
결과 > 치킨을 먹습니다.
ㄴ @Primary가 추가되면 같은 타입의 Bean이 여러 개 있더라도 우선 @Primary가 설정된 Bean 객체를 주입해줌!!!
3. @Qualifier 사용하기
Pizza
BeanTest
결과 > 피자를 먹습니다.
- 같은 타입의 Bean들에 Qualifier와 Primary가 동시에 적용되어있다면 Qualifier의 우선순위가 더 높음!!!
- 하지만 Qualifier는 적용하기 위해서 주입 받고자하는 곳에 해당 Qualifier를 반드시 추가해야함!
- 따라서 같은 타입의 Bean이 여러 개 있을 때는
범용적으로 사용되는 Bean 객체에는 Primary를 설정하고
지엽적으로 사용되는 Bean 객체에는 Qualifier를 사용하는 것이 좋음
[인증과 인가]
- 인증(Authentication)
- 해당 유저가 실제 유저인지 인증
- 스마트폰에 지문인식, 이용하는 사이트에 로그인 등과 같이, 실제 그 유저가 맞는지를 확인하는 절차
- 인가(Authorization)
- 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인. ex)관리자 페이지-관리자 권한
(로그인은 인증을 할 때(비밀번호 입력하고 제출 할 때)이고, 회원/비회원 여부에 따라 다른 권한을 받는 것이 인가)
“웹 애플리케이션 인증”은 어떠한 특수성이 있을까?
- 일반적으로 서버-클라이언트 구조로 되어있고, 실제로 이 두가지 요소는 아주 멀리 떨어져 있음
- 그리고 Http 라는 프로토콜을 이용하여 통신하는데, 그 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어짐
- 비연결성(Connectionless)
- 서버와 클라이언트가 연결되어 있지 않다는 것
- 채팅이나 게임 같은 것들을 하지 않는 이상 서버와 클라이언트는 실제로 연결되어 있지 않음
ㄴ 리소스를 절약하기 위해서 - 만약 서버와 클라이언트가 실제로 계속 연결되어있다면 클라이언트는 그렇다고 쳐도, 서버의 비용이 기하급수적으로 늘어나기 때문에 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버리고있음
- 무상태(Stateless)
- 서버가 클라이언트의 상태를 저장하지 않는다는 것
- 기존의 상태를 저장하는 것들도 마찬가지로 서버의 비용과 부담을 증가시키는 것이기 때문에
기존의 상태가 없다고 가정하는 프로토콜을 이용해 구현되어 있음 - 실제로 서버는 클라이언트가 직전에, 혹은 그 전에 어떠한 요청을 보냈는지 관심도 없고 전혀 알지 못함
어떻게 비연결성, 무상태 프로토콜에서 “유저가 인증되었다”라는 정보를 유지시켜야 한다는 과제를 어떻게 해결했는지???
▼ ▼ ▼ ▼ ▼
인증의 방식
쿠키-세션 방식의 인증
- 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식
- 인증과 관련된 아주 약간의 정보만 서버가 가지고 있게 되고 유저의 이전 상태의 전부는 아니더라도 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다는 개념
- 사용자가 로그인 요청을 보냄
- 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조
- 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣음
- 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급
- 서버는 로그인 요청의 응답으로 session-id를 내어줌
- 클라이언트는 그 session-id를 쿠키라는 저장소에 보관하고 앞으로의 요청마다 세션아이디를 같이 보냄
(주로 HTTP header에 담아서 보냄!) - 클라이언트의 요청에서 쿠키를 발견했다면 서버는 세션 저장소에서 쿠키를 검증
- 만약 유저정보를 받아왔다면 이 사용자는 로그인이 되어있는 사용자!
- 이후에는 로그인 된 유저에 따른 응답을 내어줌
JWT 기반 인증
- JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미
- JWT 기반 인증은 쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별
- 사용자가 로그인 요청을 보냄
- 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조
- 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화 해서 내보냄
- 서버는 로그인 요청의 응답으로 jwt 토큰을 내어줌
- 클라이언트는 그 토큰을 저장소에 보관하고 앞으로의 요청마다 토큰을 같이 보냄
- 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증
- 이후에는 로그인 된 유저에 따른 응답을 내어줌
[쿠키와 세션]
1. 쿠키
- 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
- 클라이언트인 웹 브라우저에 저장된 '쿠키' 를 확인
ㄴ 크롬 브라우저 기준으로 '개발자도구' - Application - Storage - Cookies 에 도메인별로 저장되어 있음
- 구성요소
- Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
- Value (값): 쿠키의 값
- Domain (도메인): 쿠키가 저장된 도메인
- Path (경로): 쿠키가 사용되는 경로
- Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됨)
2. 세션
- 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
- 서버에서 클라이언트별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트별 필요한 정보를 서버에 저장
- 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용
- 세션 동작 방식 (서버는 세션ID 를 사용하여 세션을 유지)
- 클라이언트가 서버에 1번 요청
- 서버가 세션ID 를 생성하고, 쿠키에 담아 응답 헤더에 전달
- 세션 ID 형태: "SESSIONID = 12A345"
- 클라이언트가 쿠키에 세션ID를 저장 ('세션쿠키')
- 클라이언트가 서버에 2번 요청
- 쿠키값 (세션 ID) 포함하여 요청
- 서버가 세션ID 를 확인하고, 1번 요청과 같은 클라이언트임을 인지
쿠키 다루기
auth > AuthController
package com.sparta.springauth.auth; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @RestController @RequestMapping("/api") public class AuthController { public static final String AUTHORIZATION_HEADER = "Authorization"; @GetMapping("/create-cookie") public String createCookie(HttpServletResponse res) { addCookie("Robbie Auth", res); return "createCookie"; } // 쿠키 읽기 @GetMapping("/get-cookie") public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) { System.out.println("value = " + value); return "getCookie : " + value; } // 쿠키 생성 public static void addCookie(String cookieValue, HttpServletResponse res) { try { cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행 Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value cookie.setPath("/"); cookie.setMaxAge(30 * 60); // Response 객체에 Cookie 추가 res.addCookie(cookie); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage()); } } }
1. 쿠키 생성 > public static void addCookie(String cookieValue, HttpServletResponse res)
- new Cookie(AUTHORIZATION_HEADER, cookieValue);
ㄴ Cookie에 저장될 Name과 Value를 생성자로 받는 Cookie 객체를 생성 - setPath("/"), setMaxAge(30 * 60)
ㄴ Path와 만료시간을 지정 - HttpServletResponse 객체에 생성한 Cookie 객체를 추가하여 브라우저로 반환
- 이렇게 반환된 Cookie는 브라우저의 Cookie 저장소에 저장됨
- Cookie 생성은 범용적으로 사용될 수 있기 때문에 static 메서드로 선언
2. 쿠키 읽기 > public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value)
- @CookieValue("Cookie의 Name")
ㄴ Cookie의 Name 정보를 전달해주면 해당 정보를 토대로 Cookie의 Value를 가져옴
세션 다루기
Servlet에서는 유일무이한 '세션 ID'를 간편하게 만들수 있는 HttpSession을 제공해줌
auth > AuthController
package com.sparta.springauth.auth; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; @RestController @RequestMapping("/api") public class AuthController { public static final String AUTHORIZATION_HEADER = "Authorization"; // 쿠키 생성 @GetMapping("/create-cookie") public String createCookie(HttpServletResponse res) { addCookie("Robbie Auth", res); return "createCookie"; } // 쿠키 읽기 @GetMapping("/get-cookie") public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) { System.out.println("value = " + value); return "getCookie : " + value; } // HttpSession 생성 @GetMapping("/create-session") public String createSession(HttpServletRequest req) { // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환 HttpSession session = req.getSession(true); // 세션에 저장될 정보 Name - Value 를 추가합니다. session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth"); return "createSession"; } // HttpSession 읽기 @GetMapping("/get-session") public String getSession(HttpServletRequest req) { // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환 HttpSession session = req.getSession(false); String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다. System.out.println("value = " + value); return "getSession : " + value; } // 쿠키 생성 public static void addCookie(String cookieValue, HttpServletResponse res) { try { cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행 Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value cookie.setPath("/"); cookie.setMaxAge(30 * 60); // Response 객체에 Cookie 추가 res.addCookie(cookie); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage()); } } }
1. HttpSession 생성 > public String createSession(HttpServletRequest req)
- HttpServletRequest를 사용하여 세션을 생성 및 반환 가능
- req.getSession(true)
ㄴ 세션이 존재할 경우 세션을 반환하고 없을 경우 새로운 세션을 생성 - 세션에 저장할 정보를 Name-Value 형식으로 추가
- 반환된 세션은 브라우저 Cookie 저장소에 ‘JSESSIONID’라는 Name으로 Value에 저장됨
2. HttpSession 읽기 > public String getSession(HttpServletRequest req)
- req.getSession(false)
ㄴ 세션이 존재할 경우 세션을 반환하고 없을 경우 null을 반환 - session.getAttribute(”세션에 저장된 정보 Name”)
ㄴ Name을 사용하여 세션에 저장된 Value를 가져옴
[RestTemplate]
Server To Server
- 지금까지는 Client 즉, 브라우저로부터 요청을 받는 서버의 입장에서 개발을 진행
- 서비스 개발을 진행하다보면 라이브러리 사용만으로는 구현이 힘든 기능들이 무수히 많이 존재
- 예를 들어 우리의 서비스에서 회원가입을 진행할 때 사용자의 주소를 받아야 한다면?
ㄴ 주소를 검색할 수 있는 기능을 구현해야하는데 직접 구현을 하게되면 많은 시간과 비용 - 이 때, 카카오에서 만든 주소 검색 API를 사용한다면 해당 기능을 간편하게 구현가능
- 이럴 때 우리의 서버는 Client의 입장이 되어 Kakao 서버에 요청을 진행해야함
- Spring에서는 서버에서 다른 서버로 간편하게 요청할 수 있도록 RestTemplate 기능을 제공함
▼ 프로젝트 2개를 만들어서 Client 입장의 서버는 8080 port, Server 입장의 서버는 7070 port로 동시에 실행시킬 것임!
[RestTemplate의 GET 요청]
Client 입장 서버
RestTemplateController
RestTemplateService
@Slf4j @Service public class RestTemplateService { // 1. RestTemplate을 주입받음 private final RestTemplate restTemplate; public RestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } // 2. 요청받은 검색어를 Query String방식으로 Server입장의 서버로 RestTemplate를 사용하여 요청 public ItemDto getCallObject(String query) { // 요청 URL 만들기 URI uri = UriComponentsBuilder .fromUriString("http://localhost:7070") .path("/api/server/get-call-obj") .queryParam("query", query) .encode() .build() .toUri(); log.info("uri = " + uri); ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class); log.info("statusCode = " + responseEntity.getStatusCode()); return responseEntity.getBody(); } }
- Spring의 UriComponentsBuilder를 사용해서 URI를 손쉽게 만들 수 있음
- RestTemplate의 getForEntity는 Get 방식으로 해당 URI의 서버에 요청을 진행
- 첫 번째 파라미터에는 URI, 두 번째 파라미터에는 전달 받은 데이터와 매핑하여 인스턴스화할 클래스의 타입
- 요청의 결과값에 대해서 직접 JSON TO Object를 구현할 필요없이 RestTemplate을 사용하면 자동으로 처리해줌
- 따라서 response.getBody()를 사용하여 두 번째 파라미터로 전달한 클래스 타입으로 자동 변환된 객체를 가져올 수 있음
Server 입장 서버
ItemController
ItemService
@Service public class ItemService { private final List<Item> itemList = Arrays.asList( new Item("Mac", 3_888_000), new Item("iPad", 1_230_000), new Item("iPhone", 1_550_000), new Item("Watch", 450_000), new Item("AirPods", 350_000) ); public Item getCallObject(String query) { for (Item item : itemList) { if(item.getTitle().equals(query)) { return item; } } return null; } }
ㄴ Server 입장의 서버에서 itemList를 조회하여 요청받은 검색어에 맞는 Item을 반환
요청한 Item이 여러 개라면?(위에꺼는 그냥 하나의 ItemDto, 여기는 List<ItemDto>)
Client 입장 서버
RestTemplateController
RestTemplateService
@Slf4j @Service public class RestTemplateService { private final RestTemplate restTemplate; public RestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } // 1. Item List 조회 public List<ItemDto> getCallList() { // 요청 URL 만들기 URI uri = UriComponentsBuilder .fromUriString("http://localhost:7070") .path("/api/server/get-call-list") .encode() .build() .toUri(); log.info("uri = " + uri); ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); log.info("statusCode = " + responseEntity.getStatusCode()); log.info("Body = " + responseEntity.getBody()); return fromJSONtoItems(responseEntity.getBody()); //아래에 있음 } // 받아온 JSON 형태의 String을 처리 public List<ItemDto> fromJSONtoItems(String responseEntity) { // (1) 문자열 정보를 JSONObject로 바꾸기 JSONObject jsonObject = new JSONObject(responseEntity); // (2) JSONObject에서 items 배열 꺼내기 JSONArray items = jsonObject.getJSONArray("items"); // (3) JSONArray로 for문 돌면서 상품 하나씩 ItemDto로 변환하기 List<ItemDto> itemDtoList = new ArrayList<>(); for (Object item : items) { // JSONObject에서 ItemDto로 변환하기 ItemDto itemDto = new ItemDto((JSONObject) item); itemDtoList.add(itemDto); } return itemDtoList; } }
ㄴ 결과 값이 다중 JSON으로 넘어오기 때문에 JSON To Object를 사용하지 않고 일단 String 값 그대로를 가져옴
- Server 입장 서버의 ItemResponseDto는 아래의 JSON 형태로 변환되어 전달됨
{ "items":[ {"title":"Mac","price":3888000}, {"title":"iPad","price":1230000}, {"title":"iPhone","price":1550000}, {"title":"Watch","price":450000}, {"title":"AirPods","price":350000} ] }
- JSON 처리를 도와주는 라이브러리를 추가하여 받아온 JSON 형태의 String을 처리
>> public List<ItemDto> fromJSONtoItems(String responseEntity)
ItemDto (받아온 JSONObject를 사용하여 초기화하는 생성자를 추가)
@Getter @NoArgsConstructor public class ItemDto { private String title; private int price; public ItemDto(JSONObject itemJson) { this.title = itemJson.getString("title"); this.price = itemJson.getInt("price"); } }
Server 입장 서버
ItemController
ItemService (Server 입장의 서버에서 itemList를 ItemResponseDto에 담아 반환)
@Service public class ItemService { private final List<Item> itemList = Arrays.asList( new Item("Mac", 3_888_000), new Item("iPad", 1_230_000), new Item("iPhone", 1_550_000), new Item("Watch", 450_000), new Item("AirPods", 350_000) ); public ItemResponseDto getCallList() { ItemResponseDto responseDto = new ItemResponseDto(); for (Item item : itemList) { responseDto.setItems(item); } return responseDto; } }
ItemResponseDto
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); } }
[RestTemplate의 POST 요청]
Client 입장 서버
RestTemplateController
RestTemplateService
요청 받은 검색어를 Query String 방식으로 Server 입장의 서버로 RestTemplate를 사용하여 요청
package com.sparta.springresttemplateclient.service; import com.sparta.springresttemplateclient.dto.ItemDto; import com.sparta.springresttemplateclient.entity.User; import org.json.JSONArray; import org.json.JSONObject; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.util.ArrayList; import java.util.List; import lombok.extern.slf4j.Slf4j; @Slf4j @Service public class RestTemplateService { private final RestTemplate restTemplate; public RestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } public ItemDto postCall(String query) { // 요청 URL 만들기 URI uri = UriComponentsBuilder .fromUriString("http://localhost:7070") .path("/api/server/post-call/{query}") .encode() .build() .expand(query) .toUri(); log.info("uri = " + uri); User user = new User("Robbie", "1234"); ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class); log.info("statusCode = " + responseEntity.getStatusCode()); return responseEntity.getBody(); } }
- UriComponentsBuilder의 expand를 사용하여 {query} 안의 값을 동적으로 처리가능
- RestTemplate의 postForEntity는 Post 방식으로 해당 URI의 서버에 요청을 진행
- 첫 번째 파라미터에는 URI, 두 번째 파라미터에는 HTTP Body에 넣어줄 데이터
ㄴ Java 객체를 두 번째 파라미터에 넣으면 자동으로 JSON 형태로 변환 - 세 번째 파라미터에는 전달 받은 데이터와 매핑하여 인스턴스화할 클래스의 타입
- 첫 번째 파라미터에는 URI, 두 번째 파라미터에는 HTTP Body에 넣어줄 데이터
User
package com.sparta.springresttemplateclient.entity; import lombok.Getter; @Getter public class User { private String username; private String password; public User(String username, String password) { this.username = username; this.password = password; } }
Server 입장 서버
ItemController
ItemService (Server 입장의 서버에서 itemList를 조회하여 요청받은 검색어에 맞는 Item을 반환)
package com.sparta.springresttemplateserver.service; import com.sparta.springresttemplateserver.dto.ItemResponseDto; import com.sparta.springresttemplateserver.dto.UserRequestDto; import com.sparta.springresttemplateserver.entity.Item; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; @Service public class ItemService { private final List<Item> itemList = Arrays.asList( new Item("Mac", 3_888_000), new Item("iPad", 1_230_000), new Item("iPhone", 1_550_000), new Item("Watch", 450_000), new Item("AirPods", 350_000) ); public Item getCallObject(String query) { for (Item item : itemList) { if(item.getTitle().equals(query)) { return item; } } return null; } public Item postCall(String query, UserRequestDto userRequestDto) { System.out.println("userRequestDto.getUsername() = " + userRequestDto.getUsername()); System.out.println("userRequestDto.getPassword() = " + userRequestDto.getPassword()); return getCallObject(query); } }
ㄴ 전달 받은 HTTP Body의 User 데이터를 확인
[RestTemplate의 exchange]
RestTemplate으로 요청을 보낼 때 Header에 특정 정보를 같이 전달 하고 싶다면!!!
Client 입장 서버
RestTemplateController
RestTemplateService
RestTemplate의 exchange를 사용
@Slf4j @Service public class RestTemplateService { private final RestTemplate restTemplate; public RestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } 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()); } 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; } }
ㄴ exchange 메서드의 첫 번째 파라미터에 RequestEntity 객체를 만들어 전달해주면 uri, header, body의 정보를 한번에 전달가능
Server 입장 서버
ItemController
ItemService (전달된 header와 body의 정보를 확인가능)
package com.sparta.springresttemplateserver.service; import com.sparta.springresttemplateserver.dto.ItemResponseDto; import com.sparta.springresttemplateserver.dto.UserRequestDto; import com.sparta.springresttemplateserver.entity.Item; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; @Service public class ItemService { private final List<Item> itemList = Arrays.asList( new Item("Mac", 3_888_000), new Item("iPad", 1_230_000), new Item("iPhone", 1_550_000), new Item("Watch", 450_000), new Item("AirPods", 350_000) ); public ItemResponseDto getCallList() { ItemResponseDto responseDto = new ItemResponseDto(); for (Item item : itemList) { responseDto.setItems(item); } return responseDto; } public ItemResponseDto exchangeCall(String token, UserRequestDto requestDto) { System.out.println("token = " + token); System.out.println("requestDto.getUsername() = " + requestDto.getUsername()); System.out.println("requestDto.getPassword() = " + requestDto.getPassword()); return getCallList(); } }
[Naver Open API]
"상품 검색 API" 동작 순서
naver > controller > NaverApiController
@RestController @RequestMapping("/api") public class NaverApiController { private final NaverApiService naverApiService; public NaverApiController(NaverApiService naverApiService) { this.naverApiService = naverApiService; } @GetMapping("/search") public List<ItemDto> searchItems(@RequestParam String query) { return naverApiService.searchItems(query); } }
naver > dto > ItemDto
@Getter @NoArgsConstructor public class ItemDto { private String title; private String link; private String image; private int lprice; public ItemDto(JSONObject itemJson) { this.title = itemJson.getString("title"); this.link = itemJson.getString("link"); this.image = itemJson.getString("image"); this.lprice = itemJson.getInt("lprice"); } }
naver > service > NaverApiService
@Slf4j(topic = "NAVER API") @Service public class NaverApiService { private final RestTemplate restTemplate; public NaverApiService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } public List<ItemDto> searchItems(String query) { // 요청 URL 만들기 URI uri = UriComponentsBuilder .fromUriString("https://openapi.naver.com") .path("/v1/search/shop.json") .queryParam("display", 15) .queryParam("query", query) .encode() .build() .toUri(); log.info("uri = " + uri); RequestEntity<Void> requestEntity = RequestEntity .get(uri) .header("X-Naver-Client-Id", "Client-Id") .header("X-Naver-Client-Secret", "Client-Secret") .build(); ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class); log.info("NAVER API Status Code : " + responseEntity.getStatusCode()); 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; } }
소중한 공감 감사합니다