Don't Panic
in Springboot ☕ 14분 읽기

[Springboot] Springboot 개념정리

목차

Springboot with JPA

출처: https://getinthere.tistory.com/9

핵심 용어를 먼저 정리한다.

  • IoC : Singleton으로 bean을 관리하는 Container
  • DI : 인스턴스 생성 시 IoC 컨테이너에서 singleton으로 관리하는 bean들을 변수에 의존성 주입
  • SPRING : IoC 컨테이너를 통해 bean들을 Singleton으로 관리하고, 필요에 따라 DI 해주는 프레임워크

객체 관련 기본 개념:

  • class = 설계도
  • object = 실체화가 가능한 것
  • instance = 실체화 된 것

스프링의 특징

  • 프레임워크이자 오픈소스이다.
  • IoC 컨테이너를 가지며, DI를 지원한다.
  • 엄청나게 많은 필터를 가지고 있다.
  • 엄청나게 많은 어노테이션을 가지고 있다. (리플렉션, 컴파일 체킹)
  • MessageConverter를 가지고 있다. 기본값은 현재 JSON이다.
  • BufferedReader와 BufferedWriter를 쉽게 사용할 수 있다.
  • 계속 발전 중이다.

메모리에 올라가는 객체와 DI

오브젝트를 new 해서 Heap 메모리 공간에 올리면 다음과 같이 동작한다.

의자 S = new 의자();

여기서 Snew가 만든 스택(메서드)에서 관리한다.

의자 객체를 new로 생성해 Heap에 올리고 스택에서 참조하는 구조

public void make() 같은 함수에서 의자를 new 해서 실체화하면, 메모리에 띄운 주소를 S가 가진다. 이 주소는 S가 실행 중일 때만 떠 있다.

서로 다른 메서드가 각자의 의자 객체를 생성해 공유가 어려운 상황

다른 메서드(use)에서 의자 s = new 의자()로 또 생성하면, 새 메모리를 각 메서드가 따로 관리하므로 서로 다른 의자가 된다. 그래서 공유해서 쓰기가 매우 어렵다.

스프링이 객체들을 스캔해 직접 관리하는 구조

이렇게 객체와 메서드를 스프링이 스캔해서 직접 관리하게 되는 것을 DI(제어의 역전) 라고 하며, 내가 원하는 모든 클래스의 메서드에서 이 객체를 가져와 사용할 수 있게 된다.

의자를 다른 곳에서 또 쓰고 싶으면 그냥 가져다 쓰면 된다. 아래 클래스의 의자와 위 클래스의 의자는 싱글톤이기 때문에 같은 객체다. 이렇게 필요한 것을 가져다 쓰는 것이 DI다.


필터란?

A 성에 왕의 집이 있고 B 성과 전쟁 중이라 하자. B 나라 스파이가 A 성으로 침입하면 막아야 한다. 문지기는 침입을 막는 역할을 하는데, 스프링 자체가 이런 필터 기능을 갖고 있다.

  • 필터 기능을 사용 안 하도록 / 사용하도록 설정할 수 있다.
  • 직접 필터를 생성할 수도 있다.

스프링 컨테이너가 들고 있는 필터(인터셉터) 개념도

스프링 컨테이너가 들고 있는 필터를 인터셉터라 부른다. 관련해서 AOP라는 개념도 있는데, 아직은 다루지 않는다.

어노테이션과 컴파일 체킹

어노테이션은 (주석 + 힌트)이며, 컴파일러가 무시하지 않는다.

// 글 (주석) <- 컴파일러 무시

스프링 어노테이션 객체 생성

어노테이션으로 미리 약속을 해 둔다.

  • @Component = 클래스를 메모리에 로딩
  • @Autowired = 로딩된 객체를 해당 변수에 집어넣음

스프링은 class A를 직접 new 하기보다 IoC를 사용한다. 클래스 위에 @Component가 붙어 있으면 스프링이 해당 클래스를 읽어 힙 메모리에 로딩한다.

스프링이 클래스를 스캔할 때 어떤 것이 있는지 분석하는 것을 리플렉션(reflection) 이라 한다. 리플렉션을 통해 메서드, 필드, 어노테이션 등을 체킹할 수 있고, 어노테이션이 있으면 무엇을 하라고 설정할 수 있다. 예컨대 @Autowired가 있으면 “해당 변수를 집어넣으라”고 동작한다.

@Autowired가 타입을 기준으로 IoC가 관리하는 객체를 찾아 주입하는 과정

IoC가 집어넣을 후보 객체는 많을 텐데, 스프링은 A와 동일한 타입의 객체가 떠 있는지 확인한다.

  • 같은 타입의 객체가 없다면 null이 들어간다.
  • 같은 타입이 있으면 그 타입으로 IoC가 읽어들인 객체를 찾아 @Autowired된 곳에 넣는다.

정리하면, 어노테이션은 주석 + 힌트이고, 리플렉션은 해당 클래스가 어떤 메서드·어노테이션을 갖는지 런타임에 분석하는 기법이다.


MessageConverter

서로 다른 언어를 쓰는 두 나라가 메시지를 주고받으려면 번역이 필요한 상황

영어권 사람과 한국 사람이 메시지를 주고받을 때, 각자 자기 언어밖에 못 하니 서로 번역해서 보내야 한다.

양쪽이 모두 이해할 수 있는 중간 데이터(언어)를 두는 구조

그래서 중간 데이터(중간 언어) 를 만든다. 한국어 ↔ 영어를 서로 직접 이해하긴 힘드니, 둘 다 이해 가능한 중간 언어를 둔다. 과거에는 XML을 중간 언어로 썼고, 요즘은 JSON을 많이 쓴다.

MessageConverter와 Jackson이 JSON 변환을 담당하는 구조

메시지 컨버터가 존재하며, Jackson은 JSON 데이터로 변경해 주는 라이브러리다.

  • BufferedReader = 가변 길이의 문자를 받을 수 있다.
  • @ResponseBody = BufferedWriter가 존재
  • @RequestBody = BufferedReader가 존재

JPA란

  • JPA는 Java Persistence API이다.
  • JPA는 ORM 기술이다.
  • JPA는 반복적인 CRUD 작업을 생략하게 해 준다.
  • JPA는 영속성 컨텍스트를 가지고 있다.
  • JPA는 DB와 OOP의 불일치성을 해결하기 위한 방법론을 제공한다. (DB는 객체 저장 불가능)
  • JPA는 OOP의 관점에서 모델링을 할 수 있게 해 준다. (상속, 콤포지션, 연관관계)
  • 방언 처리가 용이하여 Migration하기 좋고, 유지보수에도 좋다.
  • JPA는 쉽지만 어렵다.

영속성(persistence)

영속성은 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성이다.

출처: https://sugerent.tistory.com/587 [MISTERY]

  • RAM은 휘발성(전기로 저장)이라 컴퓨터가 꺼지면 다 사라진다.
  • 영속성은 데이터를 영구히 저장되게 해 준다.
  • 저장 시 하드디스크 파일 시스템에 저장한다고 생각하면 되고, 비휘발성이다.

API

  • Application : 프로그램
  • Programming : 프로그래밍
  • Interface : 인터페이스

프로토콜과 인터페이스를 1:1로 대응시킨 개념도

프로토콜 | 인터페이스 (1:1로 동등하다) — 둘 다 “약속”이지만 약속의 방식이 조금 다르다.

  • 인터페이스 : 상하관계가 존재하는 약속

일단 중요한 건 JPA(Java Persistence Application Programming Interface)가 무엇인지 알아두는 것이다.


ORM

ORM → Object Relational Mapping

쉽게 말하면 오브젝트를 DB에 연결하는 방법론이다. ORM은 하인 같은 존재다.

프로그램을 짜다 보면 모델 클래스들이 생긴다.

일반적인 순서 — 테이블을 먼저 만들고 클래스를 만드는 흐름

원래는 테이블을 먼저 만들고 클래스를 만드는 순서다. 그런데 이 순서를 역으로 바꿀 수 있다. 클래스를 기반으로 테이블을 만들 수 있는데, 이때 JPA가 가진 인터페이스가 필요하다. 이 인터페이스 규칙을 지키면서 클래스를 만들면 테이블이 자동 생성되게 하는 것이 ORM이다.

JPA가 반복적인 CRUD 작업을 함수로 제공하는 구조

또한 JPA는 반복적인 작업을 생략하게 해 준다. CRUD 같은 일련의 작업이 함수로 제공되므로, 반복적인 작업을 직접 하지 않아도 JPA가 처리해 준다.

영속성 컨텍스트

  • 영속성 : 데이터 → 영구적으로 저장
  • 컨텍스트(context) : 대상의 모든 정보를 의미한다고 보면 된다.

컨텍스트가 한 대상에 달라붙는 모든 정보를 가지는 개념

컨텍스트는 어떤 대상에 달라붙는 모든 정보를 가지고 있다. (예: “영숙이”의 모든 것을 알고 있다.)

DB에 직접 접근해서 저장하는 게 아니라, 중간에 영속성 컨텍스트가 있다. 영속성 컨텍스트는 DB에 저장해야 하는 모든 정보, 즉 자바 프로그램과 DB를 오가는 모든 정보를 알고 있다.

  • select 하면 DB로 바로 가는 게 아니라 영속성 컨텍스트로 가서 요청한다.
  • 영속성 컨텍스트가 정보를 안 들고 있으면 DB로 가서 데이터를 가져온 뒤 돌려준다.

DB·영속성 컨텍스트·자바 객체의 데이터가 일치된 상태

이렇게 되면 세 곳(자바 객체 / 영속성 컨텍스트 / DB)의 데이터가 일치하게 된다. 이 상태에서 자바 객체의 과일 데이터를 변경(사과 → 딸기)하면, 영속성 컨텍스트의 과일 데이터도 딸기로 변한다. 이때 DB에는 insert가 아니라 update가 자동으로 호출된다. 이런 일을 해 주는 것이 영속성 컨텍스트다.

자바는 항상 영속성 컨텍스트를 통해 저장하고, 데이터도 영속성 컨텍스트로 저장된다.

team을 DB 기준이 아닌 객체(OOP)로 모델링한 예시

team을 DB에 맞춰 int로 만들지 않고 오브젝트로 만들었다면, 이것이 OOP다. 모델을 만들 때 자바가 주도권을 쥐는 모델을 만들 수 있다. ORM을 통해 OOP의 불일치성을 해결할 수 있으며, DB는 객체 저장이 불가능하지만 자바는 가능하고 JPA가 자동으로 처리해 준다.


OOP 관점에서의 모델링

JPA로 클래스를 먼저 만들고 DB 테이블을 자동 생성하게 된다.

JPA 엔티티 클래스 정의 예시

엔티티로부터 자동 생성된 테이블 예시

무슨 DB를 쓰든 인식하고 처리할 수 있다.


HTTP란

  • 소켓 : 운영체제가 가지고 있는 것.
  • 소켓 통신 : time slice로 동시 동작

소켓 통신에서 클라이언트마다 새로운 스레드를 만드는 구조

  • 5000번 소켓에 5001이라는 새로운 스레드를 만들어 B와 통신한다.
  • C가 5000번과 통신할 때도 소켓이 만들어지며, 5002라는 새로운 스레드가 생성된다.

소켓 통신의 장점은 연결이 계속 유지되어 끊어지지 않는다는 점이다. 단, 부하가 올라가면 느려질 수 있다.

반면 HTTP 통신(문서를 전달하는 통신)은 Stateless 방식이다.

아파치와 톰캣의 역할 비교 (1)

아파치와 톰캣의 역할 비교 (2)

  • 아파치는 요청한 파일을 그대로 응답해 준다.
  • 톰캣은 요청한 파일 중 자바 코드가 요청되면 그것을 컴파일·HTML로 번역해서 돌려준다.

서블릿 생명주기

서블릿 컨테이너가 요청에 따라 서블릿 객체와 스레드를 생성하는 흐름

  • 자바 관련 요청이 오면 서블릿 컨텍스트가 서블릿 객체를 생성한다.
  • 자바 코드로 웹을 처리할 수 있게 하는 것이 서블릿이고, 이를 모아 컨테이너로 만든 것이 서블릿 컨테이너(톰캣) 이다.
  • 스레드를 생성하고 서블릿 객체에서 DB 연결 등 수많은 일을 한다. 단, 무한정 만들어지는 것은 아니다.

web.xml이란

web.xml에서 설정하는 항목은 다음과 같다.

  • ServletContext의 초기 파라미터
  • Session의 유효시간 설정
  • Servlet/JSP에 대한 정의
  • Servlet/JSP 매핑
  • Mime Type 매핑
  • Welcome File list
  • Error Pages 처리
  • 리스너/필터 설정
  • 보안

비유로 이해하는 web.xml

성에 입구가 있고, 그 입구에 문지기가 있다. 문지기는 스스로 일하지 못하고, 관리자가 던져 준 문서대로 일한다. 이 일은 관리자에 따라 달라질 수 있다. 여기서 문지기가 곧 web.xml이며, 다만 web.xml의 내용이 자주 달라질 일은 없다.

web.xml(문지기)이 설정된 문서대로 요청을 처리하는 비유

  • 초기 파라미터 : 한 번 설정하면 어디에서든 동작 가능하다.
  • 세션 유효기간 : 인증을 통해 들어온 사람이 머물 수 있는 기간이다. 3일로 설정하면 3일간 돌아다닐 수 있고, 지나면 추방된다. 더 머물려면 문지기에게 가서 세션을 갱신(초기화)해야 한다.
  • Servlet/JSP 정의 : 방문자가 가려는 목적지를 문지기에게 말하면, 문지기가 그 목적지를 찾아 안내한다.

Mime Type에 따라 데이터를 알맞은 곳으로 보내는 비유 (1)

  • Mime Type 매핑 : 방문자가 들고 온 데이터의 타입을 의미한다. 아무것도 안 들고 온 사람은 뭔가를 “보러” 온 것이므로 GET 방식(select)을 쓴다. 반대로 데이터(쌀)를 들고 오면 알맞은 창고로 보낸다. 마임 타입을 알아야 문지기가 어디로 보낼지 판단할 수 있고, 타입이 틀리면(쌀창고가 아니라 물창고로 가면) 에러가 난다. 결론적으로 들고 온 데이터가 무엇인지 알려주는 것이 Mime Type이다.
  • Welcome File list : 목적지도 모르고 데이터도 없이 온 방문자를 어디로 보낼지에 대한 정의다. 정의가 없으면 광장으로 보낸다(설정하기 나름).
  • Error Pages 처리 : 저장할 곳이 없는 데이터나 이상한 주소로 온 경우 에러 페이지로 보낸다.
  • 리스너/필터 : 출신 나라가 적국이거나 금지된 물건(총)을 들고 온 사람을 막는 것이 필터다. 리스너는 특정 조건(예: 술 잘 먹는 사람)을 가진 방문자가 들어오는지 지켜보다가, 확인되면 관리자에게 데려가는 역할이다. web.xml이 할 일이 너무 많아 일부를 리스너에게 위임한다고 보면 된다.
  • 보안 : 이상한 사람(불법 체류자 등)이 들어오면 쫓아내는 것이다.

FrontController 패턴이 필요한 이유

Servlet/JSP 매핑을 web.xml에 직접 하거나 @WebServlet 어노테이션으로 모든 클래스에 적용하면 코드가 너무 복잡해진다. 그래서 FrontController 패턴을 이용한다. 문지기(web.xml)가 할 일이 너무 많아지는 것을 줄여 주는 것이 FrontController다.


DispatcherServlet이란

FrontController 패턴

.do 같은 특정 주소 요청이 들어오면 프런트 컨트롤러로 보낸다.

요청이 톰캣을 거쳐 request/response 객체로 처리되는 흐름

  • 톰캣으로 가서 request, response 객체를 만든다.
    • request : 요청 정보를 담고 있는 객체
    • response : 응답해 줘야 하는 객체
  • web.xml에 설정된 대로, .do로 끝나는 주소를 FrontController가 낚아챈다.
  • 낚아챈 .do 요청을 자원을 찾아가도록 다시 request 한다. 재요청 시에는 request 객체를 새로 만드는 게 아니라 덮어씌우는 방법이 있다.

ApplicationContext란

web.xml -> DispatcherServlet(컴포넌트 스캐너)

디스패처 서블릿은 주소를 분배한다. 요청이 들어왔을 때 어디로 보낼지 결정하기 전에, src(소스 폴더) 안에 모인 자바 파일들을 컴포넌트 스캐너가 자동으로 com.cos.blog 같은 패키지에서 스캔한다.

DispatcherServlet이 컨트롤러 등 어노테이션 붙은 클래스를 스캔해 메모리에 올리는 구조

DispatcherServlet이 읽어 메모리에 띄우는(관리하는) 어노테이션:

  • @Controller — 컨트롤러 역할을 할 것임을 알려 줌
  • @RestController
  • @Configuration
  • @Repository
  • @Service
  • @Bean

(@Hello 같은 커스텀 어노테이션도 만들 수는 있다.) 어떤 어노테이션이 메모리에 띄워지는지 알아 둘 필요가 있다.

ContextLoaderListener

DispatcherServlet 진입 전에 하는 일이 있는데, ContextLoaderListener다.

  • 요청한 사람 수에 따라 스레드가 만들어진다(예: 20개). 각 스레드는 충돌 없이 따로 만들어진다.
  • 이렇게 스레드마다 띄우는 것보다, 모든 요청이 공통으로 쓸 수 있는 것(DB 관련 객체 등)은 ContextLoaderListener를 통해 띄운다.
  • ContextLoaderListenerroot-applicationContext 파일을 읽으며, 이 파일은 XML이나 자바 파일로 커스터마이징할 수 있다.

ContextLoaderListener가 root-applicationContext를 읽어 공통 객체를 메모리에 올리는 구조

핵심은, ContextLoaderListener가 root-applicationContext 파일을 읽어 스레드마다 따로 관리할 필요 없는 공통 객체를 메모리에 띄운다는 것이다.

DispatcherServlet 영역과 root 영역의 접근 방향 차이

DB는 DispatcherServlet이 띄운 객체에 접근하지 못한다. 반대로, DispatcherServlet이 띄운 객체는 DB에 접근할 수 있다. DB 접근은 root에 띄운 것을 가져와서 쓴다.

DispatcherServlet 정리

(6) DispatcherServlet

FrontController 패턴을 직접 짜거나 RequestDispatcher를 직접 구현할 필요가 없다. 스프링에 DispatcherServlet이 있기 때문이다. DispatcherServlet은 FrontController 패턴 + RequestDispatcher다.

DispatcherServlet이 자동 생성될 때 수많은 객체가 생성(IoC)되는데, 보통 필터들이다. 이 필터들은 직접 등록할 수도 있고, 기본적으로 필요한 필터들은 자동 등록된다.

(7) 스프링 컨테이너

DispatcherServlet에 의해 생성되는 수많은 객체들은 ApplicationContext에서 관리된다. 이것을 IoC라고 한다.

ApplicationContext

IoC란 제어의 역전을 의미한다. 개발자가 직접 new로 객체를 생성하면 그 객체를 가리키는 레퍼런스 변수를 관리하기 어렵다. 그래서 스프링이 직접 객체를 관리하며, 이때 우리는 주소를 몰라도 된다. 필요할 때 DI하면 되기 때문이다.

DI(의존성 주입) 는 필요한 곳에서 ApplicationContext에 접근하여 필요한 객체를 가져오는 것이다. ApplicationContext는 싱글톤으로 관리되므로 어디에서 접근하든 동일한 객체임을 보장한다.

ApplicationContext에는 두 종류가 있다.

a. servlet-applicationContext

  • ViewResolver, Interceptor, MultipartResolver 객체를 생성한다.
  • 웹과 관련된 어노테이션(Controller, RestController)을 스캔한다.
  • 이 파일은 DispatcherServlet에 의해 실행된다.

b. root-applicationContext

  • 위 어노테이션을 제외한 Service, Repository 등을 스캔하고 DB 관련 객체를 생성한다. (스캔 = 메모리에 로딩한다는 뜻)
  • 이 파일은 ContextLoaderListener에 의해 실행된다. ContextLoaderListener를 실행해 주는 것은 web.xml이므로, root-applicationContext가 servlet-applicationContext보다 먼저 로드된다.

따라서 servlet-applicationContext에서는 root-applicationContext가 로드한 객체를 참조할 수 있지만, 그 반대는 불가능하다. 생성 시점이 다르기 때문이다.

Bean Factory에서 getBean()으로 lazy-loading하는 구조

Bean Factory

필요한 객체를 Bean Factory에 등록할 수도 있다. 여기에 등록하면 초기에 메모리에 로드되지 않고, 필요할 때 getBean() 메소드로 호출하여 메모리에 로드한다. 이것 또한 IoC이며, 필요할 때 DI하여 사용할 수 있다. ApplicationContext와 다른 점은, Bean Factory에 로드되는 객체들은 미리 로드되지 않고 필요할 때 호출하여 로드하므로 lazy-loading이 된다는 점이다.


적절한 응답을 찾는 법

  • HandlerMapping이 적절한 함수를 찾는다.
  • 이 함수의 return이 response될 때, 요청한 쪽(hello)으로 흐른다.

web.xml이 자기가 할 일을 기억하고 DispatcherServlet에 넘겨서 일감을 주고, DB 관련 작업을 한다. 일단 추상적인 개념을 잡아 두자.