본문 바로가기

[부트캠프] kdt 심화 과정

[스파르타코딩] 스프링 입문과 숙련 주차(1)

스프링이 관리하는 빈을(Bean) 개발자가 수동으로 등록하는 방법

 

빈 생성 과정: 스프링부트 기반 프로젝트가 동작할 때 컴포넌트 스캔이 이루어지고, 

스프링이 @Component 애너테이션이 작성된 클래스를 발견하면

해당 클래스로부터 객체를 생성하여 IoC 컨테이너에서 관리한다.

그리고 프로젝트가 동작하는 동안 해당 객체가 필요한 곳에 스프링이 주입해준다.

 

빈을 수동으로 등록하는 경우

기술적인 문제 → 스프링 프레임워크에게 해당 클래스가

컨트롤러 계층임을 알리거나 서비스 계층임을 알리거나 레파지토리 계층임을 알릴 때

@Controller, @Service, @Repository 애너테이션을 사용한다.

 

공통적인 관심사를 처리할 때 사용하는 객체들을 수동으로 등록하는 것이 좋다.

 

예를 들면 비밀번호를 암호화할 때 사용하는 객체와 같이 기술적인 클래스는

개발자가 수동으로 빈으로 등록하는 것이 좋다(기술지원 Bean)

 

1. 빈으로 등록하고자 하는 객체를 반환하는 메서드 위에 @Bean 애너테이션을 작성해준다.

2. 이 Bean을 등록하는 메서드가 속해있는 class 위에 @Configuration 애너테이션을 작성해준다.


JWT(Json Web Token)

http로 웹에서 필요한 데이터를 요청하고 수신할 때 Json 형식으로 송수신하는 토큰을(데이터) 의미한다.

웹에서 사용되는 Json 형식의 토큰에 대한 표준 규격을 의미한다?

사용자의 인증 또는 인가 정보를 클라이언트와 서버간 안전하게 송수신하기 위해 사용된다.

인증(Authentication)은 신원을 확인하는 것, 예를 들면 로그인할 때 로그인하는 사용자가

기존의 회원이 맞는지 데이터베이스에 저장되어 있는 정보와 비교하여 참인지를 판별한다.

인가(Authorization)는 접근할 수 있는지의 여부를 판별한다.

ex. 회원과 비회원은 접속할 수 있는 페이지가 다르다.


 

mysql -u root -p

create database 데이터베이스이름; # 데이터베이스를 생성하는 명령어

show databases; # 존재하는 데이터베이스들을 보여주는 명령어

 

 


RestTemplate으로 get과 post 요청하기

 

서버를 구축할 때 라이브러리 사용만으로는 구현이 힘든 기능들이 존재한다.

예를 들면 사용자의 회원가입 로직을 구현할 때 사용자의 주소를 받아야 하는 상황이라면

주소를 검색할 수 있는 기능을 구현해야 하는데 직접 구현시 많은 시간과 비용이 소비된다.

이 때 카카오에서 제공하는 주소 검색 API를 사용하면 쉽게 구현할 수 있다. 

우리 웹 서버를 이용하는 사용자 입장에서 우리는 서버지만

주소 검색 기능 구현을 위해 카카오 주소 검색 api를 사용한다면

서버가 서버에게 서비스를 요청하는 상황이되고, 카카오 입장에서 우리는 클라이언트가 된다.

스프링에서는 서버에서 다른 서버로(Server To Server) 서비스를 간편하게 요청할 수 있도록

RestTemplate 기능을 제공하고 있다.

 

***RestTemplate타입 객체를 서비스단에서 사용할 때

서비스 계층인 클래스로부터 인스턴스화할 때(서비스 클래스의 생성자 호출시)

RestTemplate타입 객체를 생성하는 메서드를 갖는

RestTemplateBuilder타입 객체를 매개변수로 전달받는데

RestTemplateBuilder타입 객체를 통해 build() 메서드를 호출하고,

반환 값이 서비스 클래스의 RestTemplate타입 참조변수에 대입되는

로직이 구현되어 RestTemplate타입 객체를 사용할 수 있는 서비스 객체가 생성된다.

 

RestTemplate 객체를 통해 getForEntity() 메서드를 호출하고,

첫 번째 파라미터에 (서비스를 요청할 서버 )uri를 두 번째 파라미터에

요청 uri의 반환 값을 어떤 형식으로 포맷하여 받을 것인지 클래스 타입을 작성한다.  

→ getForEntity() 메서드는 첫 번째 파라미터로 전달받은 서버 uri에 get방식으로 요청을 보내고,

두 번째 파라미터로 전달받은 클래스타입을 기준으로 요청 서버로부터(uri) 전달받은 데이터가

역직렬화되어 객체형태로 반환된다. RestTemplate 객체가 getForEntity() 메서드를 호출하면

ResponseEntity 객체를 반환받는다. 제네릭타입에 작성한 객체를 ResponseEntity 객체로 감싸서

반환받는듯?

 

private final RestTemplate restTemplate;

/* RestTemplate 객체를 주입받는 것이 아닌
 RestTemplateBuilder 객체를 인자로 전달받아
 해당 객체를 통해 build() 메서드를 호출하여
 RestTemplate 객체를 반환받는다. */
public RestTemplateService(RestTemplateBuilder builder){
	this.restTemplate = builder.build();
}


public ItemDto getCallObject(String query){
	
    // 요청 uri 만들기
    URI uri = UriComponentsBuilder
    		// 클라이언트 입장의 서버가 서버 입장의 서버에게
            // 서비스를 요청할 uri를 설정
            .fromUriString("http://localhost:7070") 
            // 서버 입장의 서버의 컨트롤러 매칭에 사용될 단서
            .path("/api/server/get-call-obj") 
            // http://localhost:7070/api/server/get-call-obj?query=검색어
            .queryParam("query", query) 
            .encode()
            .build()
            .toUri();
            
    log.info("uri = ", uri);
    
    // RestTemplate의 getForEntity() 메서드는
    // Get방식으로 해당 uri의 서버에 요청을 진행한다.
    // 서버 입장의 서버에게 요청을 한 후 응답을 받을 때
    // 두 번째 파라미터인 ItemDto.class 타입으로 데이터가 변환되어 반환된다.
    ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);

	log.info("statusCode: "+responsEntity.getStatusCode());
    
    return responseEntity.getBody();
}

public List<ItemDto> getCallList() {
    // 요청 uri 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070") 
            .path("/api/server/get-call-list") 
            // http://localhost:7070/api/server/get-call-list
            .encode()
            .build()
            .toUri();
            
    log.info("uri = ", uri);
    
    // json 형식으로 반환되는 데이터를 문자열 타입으로 전달받는다.
    ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
    
	log.info("statusCode: "+responsEntity.getStatusCode());
    log.info("Body: "+responseEntity.getBody());
    
    return fromJSONtoItems(responseEntity.getBody());
}

// JSON 타입의 데이터를 문자열 형식으로 전달받아
public List<ItemDto> fromJSONtoItems(String data) {
	JSONObject jsonObject = new JSONObject(data);
	JSONArray items = jsonObject.getJSONArray("items");
	
    List<ItemDto> itemDtoList = new ArrayList<>();
    
    for(Object item : items){
    	ItemDto itemDto = new ItemDto((JSONObject) item);
        itemDtoList.add(itemDto);
        
        /*
        	public ItemDto(JSONObject item) {
            	this.title = item.getString("title");
                this.price = item.getInt("price");
            }
        */
    }
    
    return itemDtoList;
}

// RestTemplate으로 post요청하기
public ItemDto postCall(String query) {
    // 요청 uri 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070") 
            // PathVariable 방식으로
            .path("/api/server/post-call/{query}") 
            // http://localhost:7070/api/server/get-call-list
            .encode()
            .build()
            .expand(query)
            .toUri();
            
    // Entity 객체 생성
    User user = new User("Robbie", "1234");
    
    // post방식에서는 get방식과 다르게 body부분에 data를 전달할 수 있다.
    // restTemplate객체로 post방식으로 서버에 요청하면서 데이터 전달하기
    // : postForEntity(uri, uri로 전달할 데이터, 응답받은 데이터를 어떤 타입으로 변환할지 해당 클래스를 적는다.)
    
    // postForEntity() 메서드의 두 번째로 전달하는 파라미터에 담긴 데이터는
    // restTemplate객체에 의해서 json형식으로 바뀌어 서버 입장의 서버에게 전달된다.
    ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);
    
    // statusCode가 200이면 정상적으로 통신이 이루어진것
    // --> 데이터 송수신에 문제가 없었다는 것을 의미
    log.info("statusCode(): "+responseEntity.getStatusCode());
    return responseEntity.getBody();
}

 

 

*** 자바 프로그램에서 json형식을 다룰 때 도움을 주는 라이브러리: org.json:json:20230227 

JSON타입의 데이터를 자바기반 어플리케이션 서버에서 여러 개 받을 때

RestTemplate 객체가 갖는 getForEntity() 메서드를 호출하여 ResponseEntity 타입 참조변수에 반환 받는다.

└ 첫 번째 파라미터로 서비스를 요청할 서버의 uri,

두 번째 파라미터로 서버로부터 응답받은 데이터를 "어떤 클래스로" 변환해서 받을 건지 해당 클래스를 전달한다.

 

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

RestTemplate 객체가 get방식으로 첫 번째 파라미터로 전달받은 uri에 서비스를(데이터를) 요청: getForEntity()

그리고 두 번째 파라미터로 전달한 타입으로 응답받은 데이터를 변환한다.

(변환된) 문자열 타입의 데이터를 ResponseEntity 객체로 감싼다?

 

responseEntity.getStatusCode() 호출시 반환된 값으로

클라이언트 입장의 서버가 서버 입장의 서버에게 서비스를

요청한 결과 통신이 성공적이었는지를 판단할 수 있다.

 

responseEntity.getBody() 호출시 (객체로) 변환된 데이터가 반환된다.

 

RestTemplate객체의 exchange() 메서드

RestTemplate객체를 통해 http 통신할 때 body가 아닌 header에 데이터를 넣어 전송하고 싶다면

// ----------- 클라이언트 입장 서버의 컨트롤러 클래스
// 해당 클래스에는 @RequestMapping("/api/client") 가 작성되어 있다.
// get방식으로 http://localhost:8080/api/client/exchange-call
// 을 클라이언트(사용자)로부터 요청을 받으면 exchangeCall() 메서드가 호출된다.
@GetMapping("/exchange-call")
public List<ItemDto> exchangeCall(@RequestHeader("Authorization") String token) {
	// 클라이언트(사용자)로부터 http header에 name이 Authorization인 값을(value)
    // 문자열 타입의 변수에(String token) 담아서
    // 클라이언트 입장 서버의 서비스 객체의 exchangeCall() 메서드를 호출하면서
    // token을 전달한다.
	return restTemplateService.exchangeCall(token);
}

// ----------- 클라이언트 입장 서버의 서비스 클래스
public List<ItemDto> exchangeCall(String token){
	// 요청 url 만들기
    URL uri = UriComponentsBuilder
                .fromUriString("http://localhost:7070")
                .path("/api/server/exchange-call")
                .encode()
                .build()
                .toUri();
    log.info("uri: "+uri);
    
    User user = new User("Robbie", "1234");
    
    // restTemplate객체가 지닌 exchange() 메서드를 호출하면
    // Body가 아닌 header에 데이터를 담아서 전송할 수 있으며
    // exchange() 메서드의 첫 번째 파라미터에 ResponseEntity타입 객체를
    // 두 번째 파라미터에는 응답받은 데이터를 어떤 타입으로 포맷할 건지를 전달한다.
    
    // ResponseEntity 객체를 만드는데
    // header에 token 데이터를 담는다.
    ResponseEntity<User> requestEntity = RequestEntity
    .post(uri)
    .header("X-Authorization", token)
    .body(user);
    
    ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
    return fromJSONtoItems(responseEntity.getBody());

}

 

Open API 사용하기

네이버 검색 open api 구현하기

검색어에 대한 결과를(데이터) json형식으로 반환받을 것이다.

반환 형식에 따라 사용할 요청 url: https://openapi.naver.com/v1/search/shop.json 

 

프로토콜(통신 규약)은 https로, http 메서드는 get방식으로 파라미터 관련 정보는 하기를 참고

 

클라이언트 입장의 서버에서 서버의 입장인 서버에 서비스를 요청할 때

http 요청 헤더에 클라이언트 아이디와 클라이언트 시크릿을 추가해야 한다고 작성되어 있다.

 

코드실습

// 컨트롤러 클래스
package com.sparta.myselectshop.naver.controller;

import com.sparta.myselectshop.naver.dto.ItemDto;
import com.sparta.myselectshop.naver.service.NaverApiService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController // @Controllr + @ResponseBody
// @RequiredArgsConstructor // final 붙은 참조변수에 빈 주입
@RequestMapping("/api")
public class NaverApiController {
    private final NaverApiService naverApiService;

    public NaverApiController(NaverApiService naverApiService) {
        this.naverApiService = naverApiService;
    }

    @GetMapping("/search") // /api/search
    public List<ItemDto> searchItems(@RequestParam String query) {
        return naverApiService.searchItems(query);
    }
}

// 서비스 클래스
package com.sparta.myselectshop.naver.service;

import com.sparta.myselectshop.naver.dto.ItemDto;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.RequestEntity;
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 java.util.logging.Logger;

@Slf4j(topic="NAVER API")
@Service
// @RequiredArgsConstructor
public class NaverApiService {
    // Logger log = Logger.getLogger("NAVER API"); ?
    private final RestTemplate restTemplate;

    public NaverApiService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    /*
        @RequiredArgsConstructor 애너테이션 사용하지 않을 때
        RestTemplate 객체는 RestTemplateBuilder 객체의 build() 메서드 호출시
        반환된다. RestTemplate restTemplate = new RestTemplate(); (x)
        RestTemplate restTemplate = RestTemplateBuilder.build(); (o)
        public NaverApiService(RestTemplateBuilder builder) {
            this.restTemplate = builder.build();
        }
     */

    public List<ItemDto> searchItems(String query) {
        // 요청 url 만들기: https://openapi.naver.com/v1/search/shop.json
        URI uri = UriComponentsBuilder
                .fromUriString("https://openapi.naver.com")
                .path("/v1/search/shop")
                // 필수 파라미터인 검색어를 query가 key 값으로
                // value는 클라이언트로부터 전달받은 값이 대입되어 있는 query 변수를 사용
                .queryParam("display", 15)
                .queryParam("query", query)
                .encode() // .encode().build().toUri();
                .build()
                .toUri();

        log.info("uri: "+uri);

        // 클라이언트 입장인 서버에서 서버 입장의 서버에게 서비스를 요청할 때
        // RestTemplate타입 exchange() 메서드를 사용할 때 --> body가 아닌 header에 데이터를 담아서 보내려고 할 때
        // RestTemplate타입 exchange() 메서드 호출시 첫 번째 파라미터로 header에 데이터가 담긴 RequestEntity 객체를 전달해야 한다.
        // 그래서 하기와 같은 RequestEntity 객체를 만드는 것
        RequestEntity<Void> requestEntity = RequestEntity
                .get(uri) // get방식으로 설정
                .header("X-Naver-Client-Id", {네이버에서 발급받은 client key})
                .header("X-Naver-Client-Secret", {네이버에서 발급받은 secret key})
                .build();

        ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
        log.info("NAVER API Status Code: "+responseEntity.getStatusCode());
        return fromJSONtoItems(responseEntity.getBody());

    }

    // 네이버 서버로부터 전달받은 데이터가 ResponseEntity 객체에 담겨 반환됐고,
    // responseEntity.getBody() 로 반환받은 json 형식의 문자열 데이터를
    // 자바 객체로 변환하는 메서드
    private List<ItemDto> fromJSONtoItems(String responseEntityBodyData) {
        log.info("NaverApiService.fromJSONtoItems() 메서드 호출");
        log.info("전달받은 파라미터에 담긴 값(문자열 타입의 json데이터): "+responseEntityBodyData);

        JSONObject jsonObject = new JSONObject(responseEntityBodyData);
        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;
    }
}

 

postman으로 통신여부 확인하기

 

 

응답결과: 클라이언트(사용자)에게 List타입의 ItemDto객체들을 json타입으로 반환한다↓

[
    {
        "title": "조립<b>PC</b> 게이밍<b>컴퓨터</b> 고사양 게임용 데스크탑 게이밍<b>PC</b> 팰월드 발로란트 배그 롤 본체00",
        "lprice": 998000
    },
    {
        "title": "게이밍 조립<b>컴퓨터</b> 배그 롤 피파 오버워치2 고사양 게임용 조립<b>PC</b> <b>컴퓨터</b>본체 사무용01",
        "lprice": 298000
    },
    {
        "title": "윈도우11 게이밍조립<b>컴퓨터</b><b>PC</b> 배그디아블로4 게임용<b>컴퓨터</b> 본체 풀세트 윈도우01",
        "lprice": 479000
    },
    {
        "title": "게이밍 조립 <b>컴퓨터</b> 세트 조립<b>PC</b> 롤 발로란트 오버워치 배그 바른<b>컴퓨터</b> 본체 풀세트F11",
        "lprice": 449000
    },
    {
        "title": "일체형<b>PC</b> 디클 올인원 <b>PC</b> A238N+오피스365 쿼드코어 24형 <b>컴퓨터</b> FHD Win11",
        "lprice": 299000
    },
    {
        "title": "삼성 올인원 프로 일체형<b>PC</b> 27인치 Win11 울트라5 데스크탑 사무용 가정용 <b>컴퓨터</b> AGA-L51A",
        "lprice": 1299000
    },
    {
        "title": "삼성 일체형<b>PC</b> 올인원<b>PC</b> 데스크탑 사무용 인강용 선없는 <b>컴퓨터</b> Win11",
        "lprice": 778000
    },
    {
        "title": "미니<b>pc</b> n100 고성능 소형 <b>피씨</b> 초소형 <b>pc</b> 피코펄스",
        "lprice": 279000
    },
    {
        "title": "삼성데스크탑 <b>컴퓨터</b> 인텔 14세대 i7 CPU 프리미어 프로 배그 lol 게이밍 <b>PC</b>",
        "lprice": 1049000
    },
    {
        "title": "삼성 <b>컴퓨터</b> 본체 초고속 SSD 데스크탑 <b>PC</b> 윈도우10 사무용 01 i3 4130",
        "lprice": 189000
    },
    {
        "title": "게이밍 미니<b>PC</b> 가정용 사무용 롤 오버워치 게임도 되는 초소형 조립 <b>컴퓨터</b>",
        "lprice": 489000
    },
    {
        "title": "조립<b>PC</b> 고사양 게이밍 <b>컴퓨터</b> 게임용 영상편집용 주식용 데스크탑 롤 발로란트 본체0",
        "lprice": 810000
    },
    {
        "title": "삼성 데스크탑 <b>PC</b> 최신 i5 기업용 업무용 사무용 가정용 본체 <b>컴퓨터</b> 고사양 프리미어 프로 슬림형 조립",
        "lprice": 769000
    },
    {
        "title": "조립<b>PC</b> 게이밍 <b>컴퓨터</b> 롤 서든어택 오버워치 배틀그라운드 배그 <b>컴퓨터</b>본체",
        "lprice": 430000
    },
    {
        "title": "혜택가122만 LG일체형<b>PC</b> 27인치 i7 올인원 롤 사무용 <b>컴퓨터</b> 윈도우11 한컴오피스",
        "lprice": 1473000
    }
]