Skip to content

2차 세미나 | 키워드 과제 정리

Minjae Lee edited this page Apr 16, 2024 · 21 revisions

1. 데이터의 멱등성

멱등성(Idempotent) 기본 개념

멱등성(Idempotent)은 첫 번째 작업을 수행한 이후, 여러번 작업을 수행해도 결과가 달라지지 않는 성질을 의미합니다!
특정 숫자에 아무리 여러번 1을 곱하더라도, 그 결과가 계속 1에서 달라지지 않는 것이 "멱등하다는 것"의 예시라고 볼 수 있겠네요.

헷갈리지 말아야 할 것은 멱등성은 "작업 수행 이전과 이후가 동일한 성질"이 아니라, "처음 수행한 작업의 결과와 반복 수행한 작업의 결과가 동일한 성질"이라는 것입니다!

해당 키워드 과제가 9가지의 HTTP 메서드 설명 부분에 나오게 된 배경을 먼저 살펴볼 필요가 있다고 생각해요!

그 배경은 REST API에서 HTTP Method의 설계 원칙 중 중요하게 고려해야 할 부분 중 하나가 바로 멱등성이기 때문인데요.
HTTP Method가 자원(Resource)에 대해 어떤 작업을 수행하는지에 대해 나타내는 것이었다고 한다면, 멱등성은 (HTTP 메서드에서 나타내는) 이 작업이 서버의 상태에 어떤 영향을 미치는지에 대해 정의하는 것이라고 볼 수 있습니다.

HTTP 메서드로 알아보는 멱등성

"멱등성을 보장하는 서버"라는 말은 "클라이언트가 동일한 요청(Request)을 여러번 보내더라도 서버의 상태가 변하지 않는다는 것을 보장한다"는 말과 같습니다.
즉, 네트워크 문제 혹은 클라이언트의 실수로 수행된 중복 요청이 일어나더라도 영향을 미치지 못한다는 서버의 안정성, 그리고 데이터의 일관성을 보장한다는 뜻이죠.

멱등성이 중요한 이유는 의도하지 않은 작업의 결과를 발생시키지 않고, 안전하게 서버통신을 재시도할 수 있는 가능성을 클라이언트에게 제공해줄 수 있다는 점에 있다고 봐도 과언이 아닙니다.

각 HTTP 메서드별로 멱등성의 보장에 대해 어떤 차이가 있는지 자세하게 아래 표로 살펴봅시다.

Method 멱등성 근거
GET O 리소스를 한번 조회하나 여러번 조회하나 반환되는 응답(Response)은 동일합니다.
POST X 새로운 리소스를 생성할 때는 동일한 요청(Request)을 보내면 동일한 데이터가 반복해서 여러개 생성되므로, 멱등성이 보장되지 않습니다.
PUT O 리소스를 대체할 때 사용되므로 데이터가 한번 업데이트 되면, 여러번 동일한 요청을 보내더라도 업데이트 내용은 동일합니다. 리소스가 없는 경우 데이터를 생성할 수 있지만, 한번 데이터가 생성된 경우도 앞에서 말한 내용과 동일하므로 멱등성이 보장된다고 볼 수 있죠! (POST의 리소스 생성과는 다른 결입니다. 주의하세요!)
PATCH 리소스의 일부를 수정하는 PATCH는 멱등으로 설계할 수도, 멱등이 아니게 설계할 수도 있습니다. (내용 자체를 바꾸는 본문이면 멱등, (더하기 같은) 연산을 수행하는 본문이면 멱등이 아니겠네요!)
DELETE O 리소스가 한번 삭제되면 이미 삭제된 정보에 대해 요청을 보내도 결과는 동일할 것입니다.

2. Spring Boot의 구동 원리

이번 키워드에서 다루게 되는 내용은 2차 세미나에서 설명했던 위 내용에 살을 덧붙이는 개념들이라고 생각하면 되겠습니다.
조금 내용이 어려울 수도 있으니 꽉 붙잡고 따라오세요!

개념 정리부터 하고 갈게요!

Spring Boot는 웹 서버에서 요청을 받아와 WAS에서 요청에 대한 로직 처리를 한 후, 다시 Response를 보내는 과정을 통해 API 통신을 합니다!

  • Web Server : HTTP 요청을 처리하는 서버 -> Static Contents 제공, Dynamic Contents는 WAS로 전달

    • Static Contents : 서버에 저장되어있는 파일을 그대로 보여주면 되는 컨텐츠, 실시간으로 변하지 않습니다! -> HTML, CSS, JS, 이미지 파일
    • Dynamic Contents : 사용자의 요청에 따라 서버에서 실시간으로 생성되는 컨텐츠, 자주 변경됩니다! -> 검색 결과, 로그인 이후의 마이 페이지
  • Web Application Server (WAS) : Web Server의 기능을 포함하지만, 데이터베이스 관리나 비즈니스 로직 처리 등 Web Server의 요청으로 들어온 Dynamic Contents를 제공하는 서버 -> 웹 컨테이너로 보내면서 로직을 수행한다고 생각합시다!

    • 톰캣(Tomcat) : Spring Boot에 내장되어 있는 Java 기반 Web Application Server (WAS)
  • Web Container : Dynamic Contents를 생성하고 관리하는 역할 -> Java는 웹 구현 기술로 Servlet을 사용, 웹 컨테이너는 Servlet 컨테이너로 구현합니다.

    아래 개념은 어려우니 예시를 먼저 들어보고 아래를 이해해봅시다!

    Web Server가 카페라고 했을 때, Servlet은 바리스타와 같은 역할입니다. 주문(Request)을 받아 커피((Dynamic Page) Web)를 만들어내는 작업을 합니다.
    Servlet Container는 카페 매니저입니다. 카페(Web Server) 주문 과정의 운영을 담당하고, 바리스타(Servlet)가 잘 일할 수 있도록 환경을 만드는 역할이죠.
    Dispatcher Servlet은 카운터 직원과 같은 역할입니다. 손님으로부터 들어온 주문을 모두 다 받아 적절한 바리스타(Servlet)에게 작업을 분배합니다 (작업 분배는 정확하게 말하면 카페 매니저(Servlet Container)님이 수행해줍니다!).

    이 예시를 바탕으로 아래 개념을 이해해볼까요?

    • Servlet : Java의 Web 프로그래밍 기술, 웹 애플리케이션을 만드는 데 사용되며 자바 스레드와 MVC 패턴의 컨트롤러로서 동적인 응답을 클라이언트에 제공합니다. -> 말이 어려워서 그렇지, Client로부터 Request를 받고 적절한 처리를 해서 Response를 다시 보내는 "통로"의 개념이라 생각합시다.
    • Servlet Container : Web Server 환경에서 Servlet을 배포하고 관리하는 Application Server의 한 유형입니다. Dynamic Contents를 생성하기 위해 Servlet을 실행하며, 클라이언트의 요청에 따라 Java 코드를 사용하여 HTML을 동적으로 생성할 수 있게 됩니다.
    • Dispatcher Servlet : 클라이언트가 Request를 보내면 Servlet Container가 요청을 받게 되는데요. 이때, 가장 앞에서 모든 요청을 처리하는 Front Controller를 Dispatcher Servlet이라고 부릅니다.

그래서, Spring Boot 구동은 어떻게 되는건데?

Spring Boot는 웹 애플리케이션을 만들 때 필요한 여러 기능들을 자동으로 설정해주고, 클라이언트로부터 Request를 받으면 Dispatcher Servlet이 그 요청을 적절한 부분에 전달해서 처리하고 응답을 보내주는 것이 전반적인 구동 방식입니다.

  1. Main 실행: Spring Boot는 main()에서 시작됩니다. @SpringApplication가 붙은 클래스가 인스턴스화되고 이때 실행됩니다.
  2. 내장 서버 시작: @SpringApplication은 내장 웹 서버 Tomcat를 시작하고 애플리케이션을 배포합니다. 이 내장 서버는 애플리케이션을 독립적으로 실행할 수 있도록 해주며, 별도의 외부 서버 설정이 필요하지 않습니다.
  3. 자동 구성(Auto-configuration): Spring Boot는 개발자가 별도의 설정을 하지 않아도 자동으로 필요한 빈(Bean)을 구성합니다.
  4. 의존성 주입(Dependency Injection): DI로 애플리케이션의 객체들을 관리할 수 있도록 합니다. 앞선 세미나에서도 배웠지만, Spring Boot가 자동으로 의존성을 관리해준다는 점에서 큰 장점이 있다고 볼 수 있겠죠? @Autowired 어노테이션을 사용해서 필요한 Bean을 주입받는 상황이 이에 속합니다.
  5. Component Scan: @ComponentScan 어노테이션을 사용하여 애플리케이션의 클래스 경로에서 Spring 구성 요소들을 찾습니다. 이를 통해 Spring이 자동으로 빈을 인식하고 관리할 수 있습니다. -> @Controller, @Service, @Repository 어노테이션을 사용하여 Spring이 각 클래스를 Bean으로 등록할 수 있도록 지정하는 상황이 포함되겠네요.
  6. Application Context 생성: SpringApplication ApplicationContext를 생성하고 초기화합니다. 이는 Spring 애플리케이션의 실행 환경을 나타내며, Bean의 생명주기를 관리합니다.
  7. Dispatcher Servlet 및 Handler Mapping: Spring MVC를 사용하는 경우, Dispatcher Servlet이 생성되고 요청을 처리할 적절한 핸들러로 라우팅하기 위해 Handler Mapping이 설정됩니다. Dispatcher Servlet은 클라이언트로부터 요청을 받아 적절한 컨트롤러로 전달합니다.

잠깐 여기서! 요청이 들어온 싱황을 예시로 Dispatcher Servlet의 동작 방식을 설명해보죠!

  • 클라이언트로부터 HTTP 요청이 URL을 통해 들어오면 Dispatcher Servlet이 이 요청을 먼저 받습니다. 주어진 URL에 따라 어떤 기능을 수행해야 하는지도 판단합니다.
  • 이제 이 들어온 요청을 어떤 핸들러에게 전달할지 결정하기 위해 Handler Mapping에게 요청을 전달합니다. Handler Mapping은 요청을 처리할 컨트롤러(Controller)나 핸들러(Handler)를 찾아주는 역할을 합니다.
  • Handler Mapping이 적절한 핸들러를 찾으면 Dispatcher Servlet은 해당 컨트롤러나 핸들러를 실행합니다. 이때, Controller는 요청을 처리하고 필요한 작업을 수행합니다.
  • Controller가 요청을 처리하고 나면, Dispatcher Servlet은 컨트롤러가 반환한 데이터를 바탕으로 HTTP 응답을 생성합니다.
  • 그리고 마지막으로 Dispatcher Servlet은 생성한 응답을 클라이언트에게 전송합니다. 이러면 끝!
  1. 요청 처리: Dispatcher Servlet은 클라이언트로부터 요청을 받아 적절한 컨트롤러(Controller)로 전달합니다. 컨트롤러는 요청을 처리하고 적절한 응답을 생성하여 클라이언트에 반환합니다.
  2. 애플리케이션 실행: 이제 Spring Boot 애플리케이션이 실행되고, 내장 Web Server를 통해 클라이언트의 요청을 처리할 수 있습니다.

3. Ordinal이란?

"@Enumerated(EnumType.STRING)은 enum 타입 매핑 시 사용하는 Annotation이고,
ENUMType.STRING은 enum 이름을 데이터 베이스에 저장한다는 뜻입니다. 이때 기본 속성 값은 ENUMType.ORDINAL입니다."라는 세미나의 자료를 봤을 때, ORDINAL이

그런데 그 ORDINAL에 대해서 더 찾아보기 위해 구글링을 했을 때는 아래와 같이 "ORDINAL을 사용하지 말라"는 내용이 대부분이었습니다.
도대체 ORDINAL이 뭐길래? 어떤 위험성이 있길래, 사용을 하지말라고 하는 것일까? 지금부터 하나씩 살펴보도록 하겠습니다!

스크린샷 2024-04-14 오전 10 59 55

Ordinal() 기본 개념

Ordinal이라는 말 자체가 순서가 있는 수, 즉 서수란 의미로 해석해볼 수 있는데요.
Ordinal()은 Enum형 상수가 열거형 클래스 내에서 갖는 순서를 반환하는 메서드입니다. 순서는 클래스에서 정의된 순서대로 0부터 시작하게 되죠!

예시를 한번 살펴보겠습니다.
아래와 같이 계절 4개가 정의되어있는 열거형이 있다고 가정해봤을 때, SPRING을 시작으로 0부터 3까지의 Ordinal값을 순서대로 갖게 되는 것이죠. 참 쉽죠?

enum Season {
    SPRING, SUMMER, FALL, WINTER;
}

Ordinal() 메서드를 사용하는 것을 왜 지양해야하는 것일까?

이렇게 순서대로 정수값을 갖는 것이 무슨 문제가 있을까 싶지만, <이펙티브 자바> 책에서는 아래와 같은 이유로 Ordinal 사용에 대한 위험성을 지적하고 있습니다.
마찬가지로 위의 예시를 계속 이어서 이해해보죠!

1) 열거형 상수의 순서가 바뀌는 경우 (WINTER를 앞으로 땡기고 싶을 때)

봄, 여름, 가을, 겨울 순서대로 짜놓은 열거형을 1월부터 시작되는 년 기준으로 맞추고 싶어 겨울, 봄, 여름, 가을 순서대로 바꾸고 싶다고 가정해봅시다.

이렇게 되면, 원래 SPRING을 0으로, WINTER를 3으로 판단해 짜여있던 코드들을 모두 일일이 수정해줘야 할 것입니다.
단순히, WINTER 위치 하나의 수정이 모든 Season Enum형에서 비롯된 Ordinal값을 사용하고 있는 전체의 수정으로 이어지는 비효율적인 상황이 생기는 거죠.

2) 중간에 새로운 열거형 상수 값을 추가하고 싶은 경우 (SPRING과 SUMMER 사이 EARLY_SUMMER를 추가하고 싶을 때)

계절 4개가 아니라, 초여름, 초겨울 같은 추가적인 열거형 상수값을 추가하고 싶은 상황이 있다고 해봅시다.

이런 경우에도 위의 순서가 바뀌는 상황과 동일한 비효율적인 코드 전체의 수정이 필요한 상황으로 이어지겠죠?
"그냥 코드 전체 수정해주면 되는거 아니야?"라고 생각할 수 있지만, 실수로 일부 부분에서 변경사항 적용을 하지 못한다고 한다면 예상치 못한 버그를 발생시킬 수도 있겠네요!

3) 열거형 중간 값을 비워두고 싶은 경우 (FALL을 지우고 싶은 상황)

지구 온난화로 가을이라는 계절이 사라져 상수값을 지워야 하는 경우, 하지만 겨울은 3의 값을 그대로 사용하고 싶은 경우에는 Ordinal 2를 갖는 FALL의 값은 아무런 역할을 하지 못하는 더미(dummy) 상수가 될 것입니다.
불필요한 메모리를 사용한다는 점에서 비효율적이고, 안쓰는 상수를 명시하면서 코드의 가독성을 해칠 수도 있겠네요.

Ordinal 메서드 대신 인스턴스 필드를 사용하라. (from. <이펙티브 자바> 아이템 35 발췌)

위 문제들에 대한 해결책으로 <이펙티브 자바> 책에서는 "열거 타입 상수에 연결된 값은 Ordinal 메서드로 얻지 말고, 인스턴스 필드에 저장하자."라는 대안을 명시합니다.

말이 어려운 것처럼 보이지만, 그냥 쉽게 말해 "Ordinal로 순서를 얻지말고, 순서를 명시적으로 그냥 할당해서 사용해라"라는 의미라고 이해하면 됩니다.
이렇게 코드를 짜게 되면, Enum 상수의 순서가 바뀌더라도 / 값을 비우더라도 할당된 값과는 연관성이 없기에 위와 같은 상황이 일어나지 않을 것이겠죠!

아래 코드처럼요!

enum Season {
    SPRING(1), SUMMER(2), FALL(3), WINTER(4);

    private final int seasonValue;

    Season(int seasonValue) { this.seasonValue = seasonValue; }

    public int getMessage() { return this.seasonValue; }
}

4. Spring Bean의 생명주기

생명주기(LifeCycle)는 Bean이 생성되고, 사용(의존성 주입, 애플리케이션 동작 수행), 소멸되기까지의 전 과정을 의미합니다.

Bean이 뭔데?

  • DI (Dependency Injection) : 객체에게 필요한 의존성(다른 객체)을 객체 외부(Spring Boot에서는 "Spring Container"를 의미)에서 제공받는 것
  • Spring Container : Spring 프레임워크의 핵심 구성 요소, Java 객체들의 생명 주기를 관리하고 추가 기능을 제공하는 녀석
  • Bean : Spring Container가 관리하고 있는 Java 객체

Spring Container와 Spring Context가 같은건가요?
-> Spring Cotainer 안에는 BeanFactory(Spring Container의 기본 인터페이스, Bean의 생성과 관계설정과 같은 제어를 담당하는 IoC Object)와 ApplicationContext(BeanFactory를 상속받아 확장해 추가적인 기능을 제공하는 Object) 두 인터페이스가 들어있습니다! 그니까 Spring Container가 더 큰 개념이고, 그 안에 들어있는 구현체가 Spring Context(=Application Context)라고 말할 수 있겠네요.

위 용어들을 바탕으로 Spring Bean의 생명주기를 순서대로 이해해보죠!

Bean의 생명주기는 아래와 같이 이루어집니다!

  1. IoC Object와 Bean 생성 : Spring Container 안에 담겨있는 것이 Bean이니 컨테이너가 먼저 만들어지고, Bean 인스턴스를 생성합니다.
  2. 의존성 주입 : 생성된 Bean에 필요한 의존성이 자동으로 주입됩니다.
  3. 초기화 콜백 : Bean이 초기화됩니다. 실제 Bean이 사용되기 전에 필요한 설정을 수행합니다.
  4. Bean 사용 : 애플리케이션에서 필요한 곳에 있어 자유롭게 Spring Container로부터 가져와진 Bean이 사용됩니다.
  5. 소멸 전 콜백 : 애플리케이션이 종료되거나 Bean이 더 이상 필요하지 않는다면, Bean이 소멸되기 전에 마무리 작업을 수행할 수 있는 기회가 주어집니다.
  6. 소멸 : Bean 인스턴스는 메모리에서 해제되고 사라집니다.

각 단계를 조금 자세히 살펴볼까요?

1. Bean은 어떻게 Spring Container에 등록되는걸까?

두 가지 방법을 생각해볼 수 있습니다!

  • @Component로 선언된 클래스는 Spring Container에 의해 생성되어 Bean으로 등록됩니다. -> 해당 어노테이션을 상속받고 있는 @Controller @Service @Repository도 마찬가지로 Bean으로 등록됩니다.
  • @Configuration으로 선언된 설정 클래스를 생성하고 특정 타입을 반환하는 메서드에 @Bean 어노테이션을 붙여줍니다. -> Spring Container는 @Bean이 붙은 메서드를 찾고, 이 메서드를 호출해서 return되는 객체를 Bean으로 등록되고 싱글톤 패턴으로 인스턴스화됩니다.
// case 1. @Component로 선언된 클래스는 Bean으로 등록
@Component
public class ExampleComponent {
    // 클래스 내용
}

// case 2. @Configuration로 선언된 설정 클래스에서 @Bean이 붙은 메서드로부터 반환되는 객체가 Bean으로 등록
@Configuration
public class ExampleConfig {
    @Bean
    public ExampleBean exampleBean() { return new ExampleBean(); }
}

2. Spring Container를 통해 이루어지는 의존성 주입이란?

우리는 이미 지난 1차 세미나 키워드 과제에서 Spring의 의존성 주입 방식에 대해 공부한 적이 있죠?
이 개념을 바탕으로 @Autowired라는 어노테이션을 활용해서 Spring Container를 통한 의존성을 자동으로 주입하게 됩니다.

@Autowired는 Spring Container 안에 있는 Bean을 주입하는 역할. 다시 말해, Spring에서 의존성을 자동으로 주입해주는 어노테이션입니다.
*Spring에서는 생성자 주입 방식을 권장한다는 것! 다시 한번 강조합니다!

@Service        // @Service 어노테이션으로 인해 ExampleService는 Bean으로 등록됩니다.
public class ExampleService {
    private final ExampleRepository repository;

    @Autowired  // ExampleRepository의 Bean을 찾아서 ExampleService 생성자에 의존성을 주입하도록 지시합니다.
    public ExampleService(ExampleRepository repository) {
        this.repository = repository;
    }
}

3. 초기화 콜백과 소멸전 콜백 이해하기 : 콜백(callback)이란?

Spring Bean의 생명주기 단계를 살펴보면, 초기화 콜백과 소멸전 콜백이라는 2가지의 콜백 단계가 있던걸 확인할 수 있는데요.
그럼 여기서 콜백(callback)이란 Bean의 생명주기 내에서 특정 이벤트를 발생시키기 위해 Spring Container로부터 호출되는 메서드를 의미합니다.

그러니까 초기화 콜백은 Bean 생성과 의존성 주입이 완료되었다는 것을 알리기 위해, 다시 말해 객체를 사용할 준비가 완료되었으니 추가적인 설정이나 초기화 작업을 수행할 수 있도록 기회를 제공해주는 것이고,
소멸전 콜백은 Bean 소멸되기 전에 리소스를 해제해서 메모리 누수를 방지하고 시스템 리소스를 효율적으로 관리할 수 있도록 기회를 제공해준다고 생각하면 되겠습니다.

Bean 생명주기 콜백은 3가지 방법으로 관리할 수 있다고 합니다. 이 부분은 간단하게 이런 것들만 있다 정도로 설명하고 넘어갈게요!

  • InitializingBean 인터페이스의 afterPropertiesSet() 메서드와 DisposableBean 인터페이스의 destroy() 메서드 사용
  • Bean 등록 @Bean 어노테이션에 속성 메서드 지정 -> @Bean(initMethod="초기화메서드명", destroyMethod="소멸전메서드명")
  • @PostConstruct, @PreDestroy 어노테이션 사용 <가장 권장하는 방식입니다!>

5. Java Annotation 정리

⭐️Java Annotation 정리 Wiki 읽으러 가기⭐️

해당 내용은 내용이 길어져 별도의 Wiki Page로 분리해서 정리했습니다. 계속 추가적으로 업데이트할 예정입니다 ^__^