HTTP Method
HTTP의 멱등성
멱등성 : 수학에서 연산의 한 성질을 나타내는 것으로, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다. 이러한 멱등성의 개념은 HTTP에서도 동일하게 적용된다.
HTTP의 멱등성은 '요청의 효과'를 보고 판단한다. 동일한 요청을 한번 보내는 것과, 여러 번 보내는 것이 서로 동일한 효과를 지니면서 서버의 상태도 동일하게 남을 때 해당 HTTP Method가 멱등성을 갖는다고 이야기한다. 멱등성을 따질 때에는 서버의 상태만 바라보면 되며, HTTP 응답 Status는 신경 쓰지 않아도 된다.
정리하자면, 멱등성이 유지되어야 하는 경우 같은 요청을 여러 번 반복하더라도 서버의 상태는 변하지 않아야 한다는 것이 핵심이다.
HTTP Method의 멱등성

올바르게 구현된 REST API의 GET, PUT, DELETE 메서드는 멱등성이 보장된다. 어떤 이유일까?
- GET : 서버에 존재하는 리소스를 단순히 읽어오기만 하는 메서드이기 때문에 당연히 여러 번 수행되어도 결과값은 변하지 않는다. 마찬가지로 HEAD, OPTIONS 메소드도 조회에 대한 메소드이기 때문에 멱등하다고 할 수 있다.
- PUT : 서버에 존재하는 리소스를 요청에 담긴 내용대로 통째로 대체해버리므로 올바르게 구현하였다면 여러 번 수행되어도 결과 값은 변하지 않을 것이다.
- DELETE : 존재하는 데이터를 삭제한 결과와 이미 존재하지 않는 결과를 삭제하려는 시도에 대한 응답 코드는 서로 다르겠지만(200 OK 또는 404 NOT FOUND), 서버의 상태 자체는 변하지 않으므로 올바르게 구현되었다면 여러 번 수행되어도 멱등성이 보장될 것이다.
하지만, POST는 이야기가 다르다. POST 메서드가 호출될 때마다 데이터베이스 등에 요청된 데이터가 추가될 것이고, 이는 곧 멱등성을 위배한다. POST를 호출할 때마다 서버의 상태가 달라지기 때문이다.
PATCH는 어떨까? 결론부터 말하자면 PATCH 메서드는 항상 멱등성을 보장한다고 이야기 할 수 없다. 정확히는 PATCH는 멱등성을 보장하도록 설계할 수 있지만, 멱등성을 보장하지 않도록 설계할 수도 있다.
이 부분에서 PUT과 PATCH의 차이점이 드러난다. PUT은 요청에 대하여 모든 속성과 자원을 통째로 바꿔버리기 때문에 멱등성이 보장되지만, PATCH는 변경하고자 하는 자원의 일부에 대한 변화를 명령할 수 있기 때문이다.
{ "operation": "add", "age": 10 }
예를 들어 위 요청을 PATCH 메서드에 보낸다면, age라는 필드는 요청마다 10씩 증가하게 될 것이다. 따라서 단일 호출에 대한 응답과 다중 호출에 대한 응답에 대한 서버의 상태가 다를 것이고, 이는 곧 멱등하지 않음을 의미한다.
자바의 컴파일 과정


1. 개발자가 자바 소스코드(.java)를 작성한다.
2. 자바 컴파일러가 자바 소스파일을 컴파일한다.
- 이때 나오는 파일은 자바 바이트 코드(.class) 파일로, 아직 컴퓨터는 읽을 수 없고 자바 가상 머신(JVM)이 이해할 수 있는 코드이다.
- (바이트 코드의 각 명령어는 1byte 크기의 Opcode와 추가 피연산자로 이루어져 있다.)
3. 컴파일된 바이트 코드를 JVM의 클래스 로더에게 전달합니다.
4. 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data Area), 즉 JVM 메모리에 바이트 코드들을 올려준다.
- 클래스 로더의 세부 동작
- 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- 검증 : 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
- 준비 : 클래스가 필요로 하는 메모리를 할당한다. (필드, 메서드, 인터페이스 등)
- 분석 : 클래스의 상수 풀 내의 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- 초기화 : 클래스 변수들을 적절한 값으로 초기화한다. (static 필드)
- 런타임 데이터 영역 : JVM이 OS 위에서 실행되면서 할당 받는 메모리 영역으로 JVM 스택, PC 레지스터, 네이티브 메서드 스택은 스레드마다 하나씩 생성되고, 힙 영역, 메서드 영역은 모든 스레드가 공유해서 사용한다.
- 힙 영역 : 동적으로 생성된 객체가 저장되고 모든 스레드가 공유하는 영역으로, 가비지 콜렉터의 대상이 되는 공간이다.
- 메서드 영역 : 클래스 정보, 변수 정보, static으로 선언한 변수가 저장되고 모든 스레드가 공유한다.
- JVM 스택 영역 : 스택 프레임이라는 구조체로 새로운 메소드가 호출되면 push, 메소드가 종료되면 pop
- PC 레지스터 : 현재 수행중인 JVM의 명령어 주소를 저장하는 공간, 스레드가 어떤 부분을 어떤 명령어로 수행할지 저장
- 네이티브 메서드 스택 : JAVA가 아닌 다른 언어로 작성된 코드를 위한 공간. 즉, JNI를 통해 호출하는 C/C++등의 코드를 위한 공간
5. 실행 엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다.
- 실행 엔진 : 실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 두 가지 방식으로 실행한다.
- 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행하는 방식이다.
- JIT(Just-In-Time) 컴파일러 : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경한 뒤 해당 메서드를 더이상 인터프리팅 하지 않고 바이너리 코드로 직접 실행하는 방식이다.
- 인터프리터보다 실행 속도가 빠르다는 장점이 있다.
- JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때에만 JIT 컴파일러를 통해 컴파일하여 바이너리 코드를 생성한다.
JVM의 구조

# Class Loader

- 자바는 동적으로 클래스를 읽어오기 때문에 프로그램이 실행 중인 런타임이 되어서야 모든 코드가 JVM과 연결되는데, 이렇게 동적으로 클래스를 로딩해주는 역할을 하는 것이 클래스 로더이다.
- 자바 컴파일러가 컴파일을 한 .class파일을 묶어서 JVM이 운영체제로부터 할당 받은 메모리 영역인 Runtime Data Area로 적제한다.
- 조금 더 자세히 말하면, Runtime Data Area의 Method Area에 배치된다.
- 배치된 이후에 JVM은 Method Area의 바이트 코드를 실행 엔진에 제공하여 바이트 코드를 실행하는 것이다.
# Garbage Collector

- JVM은 가비지 컬렉터를 이용하여 더는 사용하지 않는 메모리를 자동으로 회수해준다. 개발자가 따로 메모리를 관리하지 않아도 되므로, 더욱 쉽게 프로그래밍을 할 수 있도록 도와주는 것이다.
- Heap 메모리 영역에 적재된 객체들 중에 참조되지 않은 객체들을 탐색 후 제거하는 역할을 한다.

- Heap Area는 GC가 효율적으로 관리하기 위해 3가지 영역으로 또 다시 나뉜다.
- Young Generation : 생긴지 얼마 되지 않은 객체가 저장되는 곳.
- 힙 영역이 찰수록 점차 왼쪽에서 오른쪽으로 데이터 저장이 된다.
Builder Pattern
Builder Pattern : 복잡한 객체를 생성하는 방법을 정의하는 클래스와 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴
생성해야 하는 객체가 Optional한 속성을 많이 가질 때 빛을 발하는 패턴이며, 생성 패턴 중 하나이다.
# 생성 패턴
인스턴스를 만드는 절차를 추상화하는 패턴으로, 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해 준다.
# 생성 패턴의 특징
- 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화한다.
- 이들 클래스의 인스턴스들이 어떻게 만들어지고 결합하는 지에 대한 부분을 완전히 가려준다.
즉, 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 어떻게 생성하는지 결정하는데 유연성을 확보할 수 있다.
# Builder Pattern를 사용하는 이유
Builder Pattern은 객체를 생성할 때 생성자만 사용하는 경우 발생할 수 있는 문제를 개선하기 위해 사용된다. 빌더 패턴 이외에도, 팩토리 메소드 패턴이나 추상 팩토리 패턴에서는 생성해야하는 클래스에 대한 속성 값이 많을 때 아래와 같은 이슈가 발생한다.
- 클라이언트가 팩토리 클래스를 호출할 때 파라미터로 넘겨주는 값의 타입, 순서 등에 대한 관리가 어려워져 에러가 발생할 확률이 높아진다.
- 경우에 따라 필요 없는 파라미터들에 대해서는 팩토리 클래스에 일일이 null 값을 넘겨줘야 한다.
- 생성해야 하는 sub class가 무거워지고 복잡해짐에 따라 팩토리 클래스 역시 복잡해진다.
빌더 패턴은 이런 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 리턴한다.
# BuilderPattern은 어떻게 사용될까?
빌더 패턴을 사용하지 않았을 때 ; 생성자 / setter 방식으로 객체 생성
[ 생성자 사용 ] : 필드를 초기화하는 생성자를 구현해서 객체를 생성하는 방식
→ new Pizza(”페퍼로니”, “씬”, null, null, 10000); 처럼 인자에 null을 넣게 되면 가독성이 떨어지고, 불필요한 코드가 증가한다.
public class Pizza {
private String name;
private String dough;
private String sauce;
private String topping;
private int price;
public Pizza(String name, String dough, int price) {
// 필수적으로 초기화가 필요한 필드
this.name = name;
this.dough = dough;
this.price = price;
}
public Pizza(String name, String dough, String sauce, int price) {
this.name = name;
this.dough = dough;
this.sauce = sauce;
this.price = price;
}
public Pizza(String name, String dough, String sauce, String topping, int price) {
this.name = name;
this.dough = dough;
this.sauce = sauce;
this.topping = topping;
this.price = price;
}
@Override
public String toString() {
return "name: " + name + ", " + "dough: " + dough + ", " + "sauce: " + sauce + ", " + "topping: " + topping + "price: " + price;
}
}
[ setter 사용 ] : 기본 생성자로 객체를 생성한 뒤에 setter 메서드를 활용해서 필드를 초기화하는 방식
→ 인자가 어떤 필드를 세팅하는 것인지 메서드 명으로 명시적으로 알 수 있음
→ 하지만 setter 메서드를 모두 구현함으로서 필드 값을 너무 쉽게 변경할 수 있다는 문제점이 존재함
→ 메서드 호출을 연속적으로 여러 번 해야 한다는 번거로움이 있음
public class Pizza{
private String name;
private String dough;
private String sauce;
private String topping;
private int price;
public PizzaJavaBean() {} // 기본 생성자
public String getName() {
return name;
}
public String getDough() {
return dough;
}
public String getSauce() {
return sauce;
}
public String getTopping() {
return topping;
}
public int getPrice() {
return price;
}
public void setName(String name) {
this.name = name;
}
public void setDough(String dough) {
this.dough = dough;
}
public void setSauce(String sauce) {
this.sauce = sauce;
}
public void setTopping(String topping) {
this.topping = topping;
}
public void setPrice(int price) {
this.price = price;
}
}
Pizza cheezePizza = new Pizza();
cheezePizza.setName("치즈");
cheezePizza.setDough("씬");
cheezePizza.setPrice("10000");
→ 빌더 패턴을 사용했을 때
[ 빌더 패턴 ]
→ setter 메서드에서와 같이 인자를 메서드 명을 통해 명시적으로 할당 가능
→ immutable 객체를 만들 수 있음
→ 생성자 여러 개 만들 필요가 없음
→ 체인 형식으로 사용하지 않으면 객체를 생성하는 중간에 다른 작업이 가능함
public class Pizza{
private final String name;
private final String dough;
private final String sauce;
private final String topping;
private final int price;
// 객체 생성 전, 값을 세팅해줄 Builder 내부 클래스
public static class Builder {
private String name;
private String dough;
private String sauce;
private String topping;
private int price;
// 필수로 초기화 해야하는 필드는 생성자로 값을 할당
// public Builder (String name) {
// this.name = name
// }
// pizza.Builder("이름"); 이렇게 쓰임
public Builder name(String name) {
this.name = name;
// 체인 형식으로 함수 연속 호출을 위해 자기 자신 리턴 ( .name().dough() )
return this;
}
public Builder dough(String dough) {
this.dough = dough;
return this;
}
public Builder sauce(String sauce) {
this.sauce = sauce;
return this;
}
public Builder topping(String topping) {
this.topping = topping;
return this;
}
public Builder price(int price) {
this.price = price;
return this;
}
// 값 세팅이 끝난 후 내부 클래스를 넘겨주어 본 객체에 값을 세팅해주는 메서드
public Pizza build() {
return new Pizza (this);
}
}
// 값 세팅이 끝난 후 내부 클래스를 넘겨주어 본 객체에 값을 세팅해주는 메서드
public Pizza (Builder builder) {
this.name = builder.name;
this.dough = builder.dough;
this.sauce = builder.sauce;
this.topping = builder.topping;
this.price = builder.price;
}
@Override
public String toString() {
return "name: " + name + ", " + "dough: " + dough + ", " + "sauce: " + sauce + ", " + "topping: " + topping + "price: " + price;
}
}
Pizza cheezePizza = new Pizza.Builder().name("치즈").dough("씬").price(10000).build();
Pizza.Builder pizzaBuilder = new Pizza.Builder();
pizzaBuilder.name("치즈");
pizzaBuilder.dough("씬");
// ** 다른 작업 **
pizzaBuilder.price(10000);
Pizza cheezePizza= pizzaBuilder.build();
Spring의 Annotation
접근자/설정자 자동 생성
@Getter / @Setter
xxx라는 필드에 선언하면 자동으로 getXxx()(boolean 타입인 경우, isXxx())와 setXxx() 메소드를 생성해준다.
@Getter @Setter
private String name;
위와 같이 특정 필드에 어노테이션을 붙여주면, 아래와 같이 자동으로 생성된 접근자와 설정자 메소드를 사용할 수 있어서 매우 편리하다.
user.setName("홍길동");
String userName = user.getName();
생성자 자동 생성
- @NoArgsConstructor : 파라미터가 없는 기본 생성자를 생성
- @AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자를 생성
- @RequiredArgsConstructor : final이나 @NonNull인 필드 값만 파라미터로 받는 생성자를 생성
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
@NonNull
private String username;
@NonNull
private String password;
private int[] scores;
}
// @NoArgsConstructor
User user1 = new User();
// @RequiredArgsConstructor
User user2 = new User("dale", "1234");
// @AllArgsConstructor
User user3 = new User(1L, "dale", "1234", null);
클래스 출력 함수 생성
- @ToString : toString() 메소드 생성, 추가적으로 exclude 속성을 사용하면 toString() 결과에서 특정 필드를 제거할 수 있다.
@ToString(exclude = "password")
public class User {
private Long id;
private String username;
private String password;
private int[] scores;
}
어노테이션 모음
- @EqualsAndHashCode : 자바 bean에서 동등성 비교를 위해 equals와 hashcode 메소드를 오버라이딩해서 사용하는데, 이 어노테이션을 사용하면 자동으로 메서드를 생성
- equals : 두 객체의 내용이 같은지, 동등성(equality)을 비교하는 연산자
- hashCode : 두 객체가 같은 객체인지, 동일성(identity)을 비교하는 연산자
- @Data : @Getter + @Setter + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode를 한 번에 설정해주는 어노테이션
- @Value : 불변을 의미하는 어노테이션으로, 불변 클래스를 만들 때 사용한다.
- @Value가 붙은 멤버 필드는 private 접근제어자와 final이 붙은 상수가 된다. (final이 붙기 때문에 setter는 존재할 수 없다.)
- @Log : Logger를 자동으로 생성하는 어노테이션으로, 자동으로 log 필드를 만들고 해당 class명으로 로거 객체를 생성해서 할당
- @Builder : Builder를 자동으로 생성하는 어노테이션
// @Builder 사용한 경우
@Builder
class Example<T> {
private T foo;
private final String bar;
}
// @Builder 사용하지 않은 경우
class Example<T> {
private T foo;
private final String bar;
private Example(T foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public static <T> ExampleBuilder<T> builder() {
return new ExampleBuilder<T>();
}
public static class ExampleBuilder<T> {
private T foo;
private String bar;
private ExampleBuilder() {}
public ExampleBuilder foo(T foo) {
this.foo = foo;
return this;
}
public ExampleBuilder bar(String bar) {
this.bar = bar;
return this;
}
@java.lang.Override public String toString() {
return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
}
public Example build() {
return new Example(foo, bar);
}
}
}
- @RestController : @Controller + @ResponseBody로, 기존 Controller는 View를 리턴하고 json 형식으로 데이터를 반환할 때 @ResponseBody를 추가해줘야 했는데 이런 기능을 통합시켜주는 어노테이션
- @{ }Mapping : RequestMapping을 {} 내부의 HTTP Method 요청으로 받고 처리하겠다는 뜻의 어노테이션으로, {} 안에 각 HTTP Method를 치환해서 사용 (ex | GetMapping)
- @Component : 자바 클래스를 스프링 빈으로 등록하라고 알리는 용도로 사용하며, 어노테이션을 사용하면 컴포넌트 스캔의 대상이 되어 어플리케이션 컨텍스트에 스프링 빈으로 등록 됨
- @Service : Service는 @Component를 상속한 어노테이션으로 ComponentScan의 대상이 되어 런타임 시에 스프링 빈으로 등록
- @RequestBody : 클라이언트가 보낸 request body로 값을 가져오기 위한 어노테이션으로, 해당 request를 받아오는 DTO 객체 앞에 붙여주면 된다.
- @RequestBody[데이터 타입(DTO 타입)][가져온 데이터를 담을 변수]
@PostMapping("/user")
public String register(@RequestBody final RegisterRequestDto request) {
System.out.println("성별: " + request.getGender());
System.out.println("이름: " + request.getName());
System.out.println("전화번호: " + request.getContact());
System.out.println("나이: " + request.getAge());
// 서비스 계층에 유저를 등록하는 메서드를 호출
return "유저 등록이 완료됐습니다.";
}
- @PathVariable : 클라이언트가 url 변수인 path variable로 보낸 값을 가져오기 위한 어노테이션
- @{HTTP Method}Mapping에 {변수명}
- 메소드 정의에서 쓴 변수명 그대로 @PathVariable(”변수명”)[데이터 타입][가져온 데이터를 담을 변수]로 사용 가능
@RestController
public class MemberController {
// 한 개
@GetMapping("/member/{name}")
public String findByName(@PathVariable("name") String name) {
return "Name: " + name;
}
// 여러 개
@GetMapping("/member/{id}/{name}")
public String findByNameAndId(@PathVariable("id") Long id, @PathVariable("name") String name) {
return "ID: " + id + ", name: " + name;
}
}
- @RequestParam : 클라이언트가 query parameter로 보낸 값을 가져오기 위한 어노테이션
- @RequestParam(”데이터를 받는 파라미터명”) [데이터 타입] [가져온 데이터를 담을 변수]
- HTTP 파라미터 이름이 변수 이름과 동일한 경우 @RequestParam의 value 생략 가능
@ResponseBody
@RequestMapping("/request-param-v1")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}

'✨ SOPT > server' 카테고리의 다른 글
04. Spring Security의 동작 과정과 주요 모듈 + 어노테이션 정리 (0) | 2023.05.26 |
---|---|
03. RDBMS와 JPA, JPA Hibernate 프록시에 대해 알아보자 (0) | 2023.05.05 |
01-2. Spring의 4가지 특징과 동작 과정 (0) | 2023.04.18 |
01-1. Java와 객체 지향 프로그래밍의 이해 (0) | 2023.04.12 |