새소식

TIL

[TIL] 230521 <Spring> Bean, 인증과 인가, RestTemplate & Open API

  • -
  • @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 등록 (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

Chicken 클래스에 @Primary 추가

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

Pizza 클래스에 @Qualifier("pizza")추가

BeanTest

주입하고자하는 필드에도 @Qualifier("pizza") 를 추가

결과 > 피자를 먹습니다.

  • 같은 타입의 Bean들에 Qualifier와 Primary가 동시에 적용되어있다면 Qualifier의 우선순위가 더 높음!!!
  • 하지만 Qualifier는 적용하기 위해서 주입 받고자하는 곳에 해당 Qualifier를 반드시 추가해야함!
  • 따라서 같은 타입의 Bean이 여러 개 있을 때는
    범용적으로 사용되는 Bean 객체에는 Primary를 설정하고
    지엽적으로 사용되는 Bean 객체에는 Qualifier를 사용하는 것이 좋음

 

 

 


 

  • 인증(Authentication)
    • 해당 유저가 실제 유저인지 인증
    • 스마트폰에 지문인식, 이용하는 사이트에 로그인 등과 같이, 실제 그 유저가 맞는지를 확인하는 절차
  • 인가(Authorization)
    • 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인. ex)관리자 페이지-관리자 권한

(로그인은 인증을 할 때(비밀번호 입력하고 제출 할 때)이고, 회원/비회원 여부에 따라 다른 권한을 받는 것이 인가)

 

  1. 일반적으로 서버-클라이언트 구조로 되어있고, 실제로 이 두가지 요소는 아주 멀리 떨어져 있음
  2. 그리고 Http 라는 프로토콜을 이용하여 통신하는데, 그 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어짐
  • 비연결성(Connectionless)
    • 서버와 클라이언트가 연결되어 있지 않다는 것
    • 채팅이나 게임 같은 것들을 하지 않는 이상 서버와 클라이언트는 실제로 연결되어 있지 않음
      ㄴ 리소스를 절약하기 위해서
    • 만약 서버와 클라이언트가 실제로 계속 연결되어있다면 클라이언트는 그렇다고 쳐도, 서버의 비용이 기하급수적으로 늘어나기 때문에 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버리고있음
  • 무상태(Stateless)
    • 서버가 클라이언트의 상태를 저장하지 않는다는 것
    • 기존의 상태를 저장하는 것들도 마찬가지로 서버의 비용과 부담을 증가시키는 것이기 때문에
      기존의 상태가 없다고 가정하는 프로토콜을 이용해 구현되어 있음
    • 실제로 서버는 클라이언트가 직전에, 혹은 그 전에 어떠한 요청을 보냈는지 관심도 없고 전혀 알지 못함

어떻게 비연결성, 무상태 프로토콜에서 “유저가 인증되었다”라는 정보를 유지시켜야 한다는 과제를 어떻게 해결했는지???
▼ ▼ ▼ ▼ ▼

쿠키-세션 방식의 인증

- 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식
- 인증과 관련된 아주 약간의 정보만 서버가 가지고 있게 되고 유저의 이전 상태의 전부는 아니더라도 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다는 개념

  1. 사용자가 로그인 요청을 보냄
  2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조

  3. 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣음
  4. 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급
  5. 서버는 로그인 요청의 응답으로 session-id를 내어줌

  6. 클라이언트는 그 session-id를 쿠키라는 저장소에 보관하고 앞으로의 요청마다 세션아이디를 같이 보냄
    (주로 HTTP header에 담아서 보냄!)
  7. 클라이언트의 요청에서 쿠키를 발견했다면 서버는 세션 저장소에서 쿠키를 검증
  8. 만약 유저정보를 받아왔다면 이 사용자는 로그인이 되어있는 사용자!
  9. 이후에는 로그인 된 유저에 따른 응답을 내어줌

 

JWT 기반 인증

- JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미
- JWT 기반 인증은 쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별

  1. 사용자가 로그인 요청을 보냄
  2. 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조
  3. 실제 유저테이블의 정보와 일치한다면 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화 해서 내보냄
  4. 서버는 로그인 요청의 응답으로 jwt 토큰을 내어줌

  5. 클라이언트는 그 토큰을 저장소에 보관하고 앞으로의 요청마다 토큰을 같이 보냄
  6. 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증
  7. 이후에는 로그인 된 유저에 따른 응답을 내어줌

 

 


1. 쿠키

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
  • 클라이언트인 웹 브라우저에 저장된 '쿠키' 를 확인
    ㄴ 크롬 브라우저 기준으로 '개발자도구' - Application - Storage - Cookies 에 도메인별로 저장되어 있음

  • 구성요소
    • Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
    • Value (값): 쿠키의 값
    • Domain (도메인): 쿠키가 저장된 도메인
    • Path (경로): 쿠키가 사용되는 경로
    • Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됨)

 

2. 세션

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 클라이언트별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트별 필요한 정보를 서버에 저장
  • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용
  • 세션 동작 방식 (서버는 세션ID 를 사용하여 세션을 유지)

  1. 클라이언트가 서버에 1번 요청
  2. 서버가 세션ID 를 생성하고, 쿠키에 담아 응답 헤더에 전달
    • 세션 ID 형태: "SESSIONID = 12A345"
  3. 클라이언트가 쿠키에 세션ID를 저장 ('세션쿠키')

  4. 클라이언트가 서버에 2번 요청
    • 쿠키값 (세션 ID) 포함하여 요청
  5. 서버가 세션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를 가져옴

 

 


 

  • 지금까지는 Client 즉, 브라우저로부터 요청을 받는 서버의 입장에서 개발을 진행
  • 서비스 개발을 진행하다보면 라이브러리 사용만으로는 구현이 힘든 기능들이 무수히 많이 존재
  • 예를 들어 우리의 서비스에서 회원가입을 진행할 때 사용자의 주소를 받아야 한다면?
    ㄴ 주소를 검색할 수 있는 기능을 구현해야하는데 직접 구현을 하게되면 많은 시간과 비용
  • 이 때, 카카오에서 만든 주소 검색 API를 사용한다면 해당 기능을 간편하게 구현가능

  • 이럴 때 우리의 서버는 Client의 입장이 되어 Kakao 서버에 요청을 진행해야함
  • Spring에서는 서버에서 다른 서버로 간편하게 요청할 수 있도록 RestTemplate 기능을 제공함

 


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을 반환

 

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);
	}
}

 

 

 


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 형태로 변환
    • 세 번째 파라미터에는 전달 받은 데이터와 매핑하여 인스턴스화할 클래스의 타입

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으로 요청을 보낼 때 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();
	}
}

 

 


Client - Postman 사용 / Server - 구현할 것

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;
	}
}

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.