다음은 글은 infrean의 "자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]" 강의의 학습 목적으로 작성된 것입니다. 강의의 디테일한 내용이나, 코드 등은 빠져있을 수 있습니다.
0. 목표
1. 스프링 프로젝트를 설정하여 실행할 수 있다.
2. 서버란 무엇인지, 네트워크와 HTTP, API는 무엇인지, JSON은 무엇인지? 등 서버 개발에 필요한 다양한 개념을 이해한다.
3. 스프링 부트를 이용하여 간단한 GET, POST API를 만들 수 있다.
1. 스프링 프로젝트 시작하기
기존의 스프링 프로젝트를 다운로드하여 시작할 수도 있지만,
Spring Initializr를 이용하여 새로운 프로젝트를 시작하는 방법을 알아보자.
해당 사이트로 접속하면, 다음과 같은 화면을 만나볼 수 있다.
각각의 항목에 대해 알아보자.
1) Project
: 프로젝트에서 사용될 빌드 툴 을 설정하는 것이다.
최근에는 Gradle을 가장 많이 사용하므로 Gradle-Groovy를 선택했다. (보편적인 Gradle이다.)
(둘의 차이는 다음 링크를 참고해 보자...)
2) Language
: 최근에는 Kotlin을 사용하는 경향이 있지만, 보편적으로 JAVA로 만들어진 프로젝트가 다수 있기 때문에,
JAVA로 진행했다.
3) Spring Boot
: 스프링부트의 버전을 고르는 항목이다.
알파벳이 붙어있으면, "개발 중 혹은 오픈 베타"라는 뜻
알파벳이 붙어있지 않으면, "안정화된 릴리즈 버전"이라고 생각하면 편하다.
4) Project Metadata
Group : 프로젝트 그룹
Artifact : 최종 결과물의 이름
Nmae : 프로젝트가 이름
Description : 프로젝트 설명
Package name : 패키지 이름
- Packaging : Spring boot는 톰캣이 내장되어 있어서 Jar을 선택하면 된다.
- Javva 버전은 : 11이 가장 대중화되어 있다.
5) Dependencies
프로젝트의 build.gradle에 있는 의존성을 추가하는 부분이다.
프로젝트에 사용하는 외부 라이브러리/ 모듈 등을 설치할 수 있다.
해당 강의에서는
'Spring Data JPA', 'H2 Database', 'Spring Web'을 사용하고, 각 의존성에 대해서는 강의 중에 설명할 예정
2. @SpringBootApplication과 서버
main. java 폴더 내의 com.group.libraryapp에 있는 LibraryAppApplication 클래스를 클릭해 보자
package com.group.libraryapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LibraryAppApplication {
public static void main(String[] args) {
SpringApplication.run(LibraryAppApplication.class, args);
}
}
이 코드를 한줄한줄 이해해 보자.
일단 가장 눈에 띄는 "@SpringBootApplication"은 무엇일까??
2-1. Annotation
어노테이션(Annotation)은 자바 프로그래밍 언어의 문법 중에 하나이다.
소스 코드에 메타데이터를 추가하는 방법을 제공한다.
어노테이션은 주로 코드의 문서화, 컴파일 경고 억제, 런타임 처리 등 다양한 용도로 활용된다.
어노테이션은 @ 기호를 사용하여 표현된다.
우리가 스프링부트가 아니어도 자바에서 자주 사용했던 내장 어노테이션 들도 있다.
특히 @Override는 메서드가 슈퍼 클래스의 메서드를 오버라이딩하고 있는지를 체크하도록 지정한다.
또한, 스프링 프레임워크와 같은 프레임워크에서는 어노테이션을 활용하여 빈 등록, 트랜잭션 관리, 컴포넌트 스캔 등과 같은 기능을 지원한다.
예를 들어, @Autowired, @Service, @ComponentScan과 같은 어노테이션들이 스프링에서 사용되며, (이후에 계속 나올 예정)
사용자 정의 어노테이션을 만들 수도 있으며, 이를 통해 프로그래머가 원하는 특정한 메타데이터나 동작을 코드에 부여할 수 있다.
2-2. @SpringBootApplication
@SpringBootApplication 어노테이션은 여러 스프링 설정들을 포함하는 편리한 어노테이션이다.
@SpringBootApplication: 이 어노테이션은 세 가지 어노테이션의 조합이다.
@Configuration, @EnableAutoConfiguration, @ComponentScan을 포함하고 있다.
이 어노테이션을 통해 스프링 부트 애플리케이션의 메인 클래스에 해당하는 클래스가 스프링 컨텍스트에 등록되며, 자동 구성을 활성화하고 컴포넌트 스캔을 수행한다.
public class LibraryAppApplication: 실제로 실행되는 스프링 부트 애플리케이션의 메인 클래스이다.
main 메서드에서는 SpringApplication.run()을 호출하여 애플리케이션을 실행한다!!!
이 메서드는 스프링 컨텍스트를 초기화하고 필요한 빈들을 등록하는 역할을 한다.
즉, RunTime 때에는 이 코드 한 줄이 돌아가는 것이다!!
따라서 개발자가 직접 설정해야 하는 부분은 상당히 감소하고, 이러한 자동화는 스프링 부트를 통해 빠르게 개발하고 유지보수하기 쉬운 애플리케이션을 만들 수 있도록 도와준다.
2-3 서버란?
아래 글에서 참고.
3. 네트워크란?
이전에 포스팅했던 글에서 참고하자.
4. HTTP와 API란?
이 내용 또한 다음 글에서 자세히 설명해 놓았다.
https://jinhos-devlog.tistory.com/entry/HTTPHTTPS-%ED%86%B5%EC%8B%A0-RESTful-API
HTTP의 형식 예시를 한 가지만 들어보자.
// 응답 ex)
HTTP/1.1 200 OK 첫째줄 : 상태코드
Content-Type: application/json 여러 줄 : 헤더
한 줄 띄기
{ 여러 줄 : 바디
"name": "A",
"age": null
}
5. GET API 개발하고 테스트하기
5-1. API 개발하기 전에 알아야 할 것 요소
API 명세(spec)
- HTTP Method (HTTP 메서드): 예시) GET, POST, PUT, DELETE
- HTTP Path (HTTP 경로): 예시) /users, /products/123
- 쿼리 (Query): 예시) /search?query=example&limit=10
- API의 반환 결과: 예시) JSON 형식의 응답
{
"status": "success",
"data": {
"user": {
"id": 123,
"name": "John Doe",
"email": "john.doe@example.com"
}
}
}
5-2. GET API 개발
1) @RestController
@RestController 어노테이션을 해당 클래스 위에다가 적어준다.
(어노테이션 위치가 가장 중요하다. 해당 클래스, 메서드, 필드 위에 작성한 어노테이션은 해당 클래스 전체에 적용된다.)
@RestController 어노테이션은 @Controller와 @ResponseBody 어노테이션을 모두 포함하고 있다.
컨트롤러 클래스를 정의할 때 사용된다.
API의 진입 시점을 요청할 수 있다.
2) @GetMapping
@GetMapping은 HTTP GET 메서드를 처리하는 핸들러 메서드를 정의할 때 사용된다.
괄호와 함께 쓰이고, ("/~") path에 대한 GET 요청을 처리하도록 한다.
이는 경로 변수, 쿼리 파라미터 등을 처리할 수 있다.
+ 추가 : @RequestParam과 @PathVariable의 차이
- URL를 통해 전달된 값을 전달하는 방식은 2가지가 있다.
- 1) http://localhost:8000/board?query=10
- 2) http://localhost:8000/board/1
- 1번의 경우 쿼리 파라미터로 전달하는 경우이고,
2번의 경우 경로 변수로 전달하는 경우이다. - 1번의 경우 : "/board?query=10"에서 'query'를 추출한다.
- 이 경우 @RequestParam 를 사용
- 2번의 경우 : "/board/{boardId}"에서 '{boardId}'를 추출한다.
- 이 경우 @PathVariable 를 사용
(🧑🏻💻 고려사항 : 현재 addTwoNumbers 메서드의 반환 타입은 int로 되어 있다.
RESTful API에서는 일반적으로 JSON 형식의 데이터를 반환하는 것이 일반적이다.
따라서 메소드의 반환 타입을 int에서 ResponseEntity로 변경하는 것이 좋지만, 배우는 과정에서 편의를 위해 다음과 같이 작성한다.)
@RestController
public class CalculatorController {
@GetMapping("/add") // GET /add
public int addTwoNumbers(@RequestParam int number1,@RequestParam int number2) {
return number1 + number2;
}
}
=> GET /add?number1=10&number2=20 과 같은 형태로 쓰일 수 있다.
포스트맨을 통해 확인해 보면, 잘 작동하는 것을 볼 수 있다.
3) DTO
또 다른 고민 : 확장성
현재는 2가지 파라미터만 받도록 되어있지만, 더 늘어난다면 수정에 용이하지 못하다.
이러한 경우를 대비해 객체를 받도록 할 수 있다.
이러한 경우 사용하는 것이 DTO!!
DTO(Data Transfer Object)를 사용하여
여러 파라미터를 하나의 객체로 묶어서 받도록 해보자.
이렇게 하면 API의 확장성이 향상되며, 새로운 필드가 추가되더라도 기존 코드를 수정하지 않고도 새로운 필드를 처리할 수 있다!
@RestController
public class CalculatorController {
// dto로 전달하기
@GetMapping("/add") // GET /add
public int addTwoNumbers(CalculatorAddRequest request) {
return request.getNumber1() + request.getNumber2();
}
}
다음과 같이, 객체로써 받았기 때문에 내부의 private 멤버 변수는 getter 메서드로 가져와야 한다.
6. POST API 개발하고 테스트하기
6-1. GET API 와의 차이
이전에는 쿼리를 사용하여 데이터를 받았다.
POST API와 같은 경우에는 HTTP Body를 이용한다.
* 이유!!
GET은 주로 쿼리 매개변수를 이용하고, POST는 주로 Request Body를 이용한다.
- GET
캐싱 가능성: GET 요청은 캐싱이 가능하다. 동일한 GET 요청은 항상 동일한 결과를 반환하므로, 결과를 캐싱하여 이후 동일한 요청에 대해 서버로의 부담을 줄일 수 있다.
북마크 및 공유: GET 요청은 URL에 쿼리 매개변수를 포함하고 있으므로, 브라우저의 주소 표시줄에 직접 나타낼 수 있고, 북마크로 저장하거나 다른 사용자에게 공유하기 용이하다. - POST
보안 및 데이터 길이 제한: POST 요청은 HTTP Body를 통해 데이터를 전송하므로, URL에 노출되지 않아 보안적인 이점이 있다.
또한 POST는 GET과 달리 데이터 길이에 제한이 없어 더 많은 양의 데이터를 전송할 수 있다.
요청의 의도 표현: POST는 주로 서버에 리소스를 생성하거나 업데이트할 때 사용된다.
따라서 데이터를 Request Body에 담아서 전송하면, 클라이언트의 의도가 명확하게 표현될 수 있다.
그렇다면 POST에서 Body로 데이터를 어떻게 받을까?
6-2. JSON
https://jinhos-devlog.tistory.com/entry/JSON%EC%9D%B4%EB%9E%80
해당 포스트를 보면 더 자세히 알 수 있다.
Java로 비유해 보자면, Map<Object, Object>의 느낌이다.
6-3. 곱셉기능을 POST API로 만들어보기
// 곱셉 POST API
@PostMapping("/multiply")
public int multiplyTwoNumbers(@RequestBody CalculatorMultiplyRequest request){
return request.getNumber1() * request.getNumber2();
}
@RequestBody : HTTP 요청의 본문(body)을 메서드 파라미터로 매핑하는 데 사용
public class CalculatorMultiplyRequest {
private int number1;
private int number2;
public int getNumber1() {
return number1;
}
public int getNumber2() {
return number2;
}
}
다음과 같이 Request DTO를 짜준 후에 PostMan으로 실행해 보자.
이번에는 Params가 아닌, Body에 JSON 형식을 담아서 보내면,
결과가 잘 수행되는 것을 볼 수 있다.
6-4. GET과 POST
강의에서는 단순히 비슷한 기능을 GET과 POST로 나누어 수행해 보았다.
하지만, 엄연히 둘은 다른 HTTP Method이고 하는 기능이 다르다.
쉽게 이야기하면 GET은 서버에서 어떠한 데이터를 가져와서 보여줄 때 사용하고,
POST는 서버 상의 데이터 값을 수정할 때 사용된다.
즉, GET의 READ의 개념이고, POST는 WRITE Able의 개념이다.
이에 따라 사용하는 어노테이션이나 활용도가 달라질 수 있다.
*GET VS POST
캐시 | ⭕️ | ❌ |
---|---|---|
브라우저 기록 | ⭕️ | ❌ |
북마크 추가 | ⭕️ | ❌ |
데이터 길이 제한 | ⭕️ | ❌ |
HTTP 응답 코드 | 200(Ok) | 201(Created) |
언제 주로 사용하는가? | Resource 요청 | Resource 생성 |
리소스 전달 방식 | Query | HTTP Body |
idempotent (멱등성) | ⭕️ | ❌ |
* 강의 내용 중에 궁금증이 생겼다.
Q1) 왜 GET API 에만 final 키워드를 붙이고, POST API에는 붙이지 않는가?
A1)
final 키워드를 필드에 붙이면, 한 번 초기화된 값은 더 이상 변경할 수 없다.
GET은 READ의 개념이라고 했으므로, 최초 객체 생성 이후에 필드가 변경될 일이 없다.
이로써 final 키워드로 보안성과 안정성을 챙길 수 있다.
Q2) POST API에서는 생성자가 없어도 값이 바인딩되는 건가? 왜 생성자를 쓰지 않아도 되는가?
A2)
GET API와 POST API에서 DTO를 사용할 때 값을 바인딩하는 방식이 다르기 때문에 발생하는 차이이다.
GET API에서 사용되는 DTO의 경우, POST와 달리 생성자를 통해 값이 바인딩된다.
(만약 생성자가 없다면, 쿼리가 바인딩되는 것이 아닌, default 값인 0이 필드에 들어가게 된다.)
허나, POST API에서 사용되는 DTO의 경우, setter를 통해 값이 바인딩되기 때문에, 생성자가 없더라도 필드에 값을 넣을 수 있다.
직전에 @RequestBody 어노테이션을 사용하면 HTTP POST 요청의 본문(body)을 자바 객체로 매핑할 수 있다고 했다.
Spring은 기본적으로
1. 객체를 생성할 때 디폴트 생성자를 호출하여 객체를 초기화하고,
2. 그 후에 setter 메서드를 사용하여 필드에 값을 설정한다.
따라서 @RequestBody를 사용하는 경우에는 디폴트 생성자가 없더라도 Spring이 객체를 생성하고 값을 바인딩할 수 있다.
(하지만 만약 클래스에 매개변수를 갖는 생성자가 명시적으로 정의되어 있다면,
Spring은 해당 생성자를 사용하여 객체를 초기화하려고 시도할 것이다.)
7. 유저 생성 API 개발
- HTTP Method : POST
- HTTP Path : /user
- HTTP Body (JSON)
{
"name": String (null),
"age": Integer
}
- 결과 반환 X (HTTP 200 OK )
8. 유저 조회 API 개발
- HTTP Method : GET
- HTTP Path : /user
- 쿼리 : X
- 결과 반환
[{
"id": Long,
"name": String (null ),
"age": Integer
}, ...]
8-1. Controller에서의 객체 반환
Controller에서 getter가 있는 객체를 반환하면 JSON이 된다.
@RestController 어노테이션을 사용한 클래스에서는 메서드의 반환값이 JSON 형태로 클라이언트에게 전송된다!!!
8-2. 코드에서 확인
public class Fruit {
private String name;
private int price;
public Fruit(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
@RestController
public class UserController {
@GetMapping("/fruit")
public Fruit fruit() {
return new Fruit("바나나", 2000);
}
}
해당 url로 GET 요청을 보내보니,
다음과 같이 JSON 형식으로 Response Body에 담겨온 것을 확인할 수 있다.
8-3. ID란 무엇인가?
위에서 id라는 필드가 존재한다.
일반적으로 id란 "데이터별로 겹치지 않는 고유한 번호"를 의미한다.
User별로 고유한 번호를 API응답 결과로 반환해주어야 한다.
여기까지가 1장의 내용이다.
허나, 우리가 만든 API의 문제점은 "유저 정보가 메모리에서만 유지되고 있기 때문에 서버를 재시작할 경우, 데이터가 모두 날아간다"는 문제가 있다.
=> 고로, 우리는 다음시간에 Database를 다루는 방법을 배울 것이다.