Back-End/Spring

Spring 입문

Kr1 2026. 4. 6. 15:21

 

 

 

그레이들(Gradle)

  • Gradle은 빌드 자동화 시스템입니다.
  • 우리가 작성한 Java 코드를 설정에 맞게 자동으로 Build 해 줍니다.
    • Build소스 코드를 실행 가능한 결과물로 만드는 일련의 과정을 뜻합니다.
    • Gradle을 사용하면 간편하게 Java 소스 코드를 실행한 가능한 jar 파일로 만들어줍니다.

build.gradle

  • build.gradle은 Gradle 기반의 빌드 스크립트입니다.
  • 이 스크립트를 작성하면 소스 코드를 빌드하고 라이브러리들의 의존성을 쉽게 관리할 수 있습니다.
  • groovy 혹은 kotlin 언어로 스크립트를 작성할 수 있습니다.
  • 우리가 개발을 하면서 필요로 하는 외부 라이브러리들을 dependencies 부분에 작성하면 Gradle이 해당 라이브러리들을 Maven Repository와 같은 외부 저장소에서 자동으로 다운로드해 옵니다.
    • Maven Repository는 라이브러리들을 모아둔 저장소입니다
  • 또한 다른 라이브러리들과의 의존성을 자동으로 관리해 주기 때문에 라이브러리들 간의 충돌 걱정 없이 개발에만 집중할 수 있습니다.
    • 라이브러리필요한 기능들이 모여있는 코드의 묶음이라 생각하시면 됩니다.
    • 개발자들은 모든 기능을 전부 직접 구현하지 않고 미리 작성되어 있는 라이브러리들을 사용하여 기능을 빠르고 쉽게 구현할 수 있습니다.

 

서버

📌 네트워크는 여러 대의 컴퓨터 또는 장비가 서로 연결되어서 정보를 주고받을 수 있게 도와주는 기술입니다.
우리는 사용자가 요청을 했을 때 해당 요청에 대한 응답을 수행하는 프로그램 즉, 서버를 개발하게 될 겁니다.

Client와 Server

  • 사용자는 브라우저를 이용하여 서버에 정보를 요청하고 응답을 받습니다.
  • 이때 사용자의 요청이 서버에 도달하기 위해서는 해당 서버의 정보가 필요합니다.
  • 이때 사용자의 요청이 해당 서버에 정확하게 도달할 수 있게 제공되는 정보IP 주소입니다.
    • 거대한 네트워크망에서 여러분의 컴퓨터를 식별하기 위한 위치 주소입니다.
    • 네트워크상에서의 데이터 송/수신은 이 주소를 기준으로 이루어지고 있습니다.
  택배 네트워크
주소(IP) 서울시 **구 **로 ***** 192.168.**.*
받는 사람(포트) Robbie 8080
네트워크에서도 정보를 요청받고 전달하려면 주소에 해당하는 IP와 받는 사람에 해당하는 포트번호를 알려줘야 합니다.

 

웹 서버

  • 웹 서버는 인터넷을 통해 HTTP를 이용하여 웹상의 클라이언트의 요청을 응답해 주는 통신을 하는 일종의 컴퓨터입니다.

웹 서버의 기본 동작 원리

  1. 브라우저를 통해 HTTP Request로 웹사이트를 웹서버에 요청합니다.
  2. 이후 웹서버는 요청을 승인하고 HTTP Response를 통해 웹사이트 데이터를 브라우저에 전송합니다.
  3. 마지막으로 브라우저는 서버에서 받아온 데이터를 이용해 웹사이트를 브라우저에 그려내는 일을 합니다.
  • 기본적으로 브라우저가 웹서버에 요청을 할 때는 항상 GET method로 요청하게 됩니다.

 

API

  • API(application programming interface)는 다른 소프트웨어 시스템과 통신하기 위해 따라야 하는 규칙을 정의합니다.
  • 인터페이스(interface)는 서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면을 의미합니다. 즉, 사용자가 기기를 쉽게 동작시키는데 도움을 주는 시스템을 의미합니다.
📌 쉽게 표현해 보자면 API하나의 "약속"입니다.
서로 다른 애플리케이션이 약속한 방식의 API 요청을 수행하면 정해진 결과물을 반환합니다. 주문을 받으면 해당 주문의 음식을 가져다주는 식당의 점원이라고 비유적으로 이해하시면 좋을 것 같습니다.

 

RESTful API

  • Representational State Transfer(REST)는 API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처입니다.
    • REST는 처음에 인터넷과 같은 복잡한 네트워크에서 통신을 관리하기 위한 지침으로 만들어졌습니다.
    • REST 아키텍처 스타일을 따르는 APIREST API라고 합니다
    • REST 아키텍처를 구현하는 웹 서비스RESTful 웹 서비스라고 합니다.
  • 조금 일반적으로 쉬운 말로 바꾸면, 여러분 서버의 api가 적절하게 http를 준수하며 잘 설계되어 있으면 RESTful 하게 설계되어 있다고 생각하시면 좋습니다.
    • 예를 들어 API의 리소스 식별자를 중복 없이 고유하게 잘 만들고
    • 해당 API에 적절하게 HTTP 메서드를 사용했다면, RESTful 하게 설계했다고 볼 수 있습니다.
  •  

 

Apache Tomcat

Web Server와 Web Application Server(WAS)

  • 브라우저에서 URL을 입력하여 어떠한 페이지를 요청했을 때 HTTP의 요청을 받아들여 HTML 문서와 같은 정적인 콘텐츠를 사용자에게 전달해 주는 역할을 하는 것Web Server입니다.
  • 웹 서버의 역할을 크게 2가지로 구분해 보면
    1. 정적인 콘텐츠 즉, 이미 완성이 되어있는 HTML과 같은 문서를 브라우저로 전달합니다.
    2. 브라우저로부터 ‘로그인하여 MyPage를 요청’과 같은 동적인 요청이 들어왔을 때 웹 서버 자체적으로 처리하기 어렵기 때문에 해당 요청을 WAS에 전달합니다.
  • 종류로는 Apache, Nginx 등이 있습니다.

 

  • WAS는 웹 서버와 똑같이 HTTP 기반으로 동작이 됩니다.
  • 웹 서버에서 할 수 있는 기능 대부분을 WAS에서도 처리할 수 있습니다.
  • WAS를 사용하면 로그인, 회원가입을 처리하거나 게시물을 조회하거나 정렬하는 등의 다양한 로직들을 수행하는 프로그램을 동작시킬 수 있습니다.
  • 종류로는 Tomcat, JBoss 등이 있습니다.

 

Apache Tomcat

  • Tomcat은 동적인 처리를 할 수 있는 웹 서버를 만들기 위한 웹 컨테이너입니다

  • Apache Tomcat이란 Apache와 Tomcat이 합쳐진 형태로 정적인 데이터 처리와 동적인 데이터 처리를 효율적으로 해줄 수 있습니다.

 

SpringBoot와 Spring

  • Spring 프레임워크는 2004년에 1.0이 등장한 이후 20년 가까이 사랑받으며 계속해서 업그레이드해 온 현재는 Spring 6.0이 등장한 아주 오래되고 강력한 프레임워크입니다.

  • Spring 프레임워크는 AOP, IoC/DI 등과 같은 아주 강력한 핵심 기능들을 가지고 있습니다.

  • 하지만 이러한 핵심 기능들을 사용하기 위해서는 너무나도 많은 xml 설정들이 필요했습니다.
  • 이러한 불편한 점들을 개선하기 위해 2014년 SpringBoot가 등장했습니다.

  • SpringBoot는 기존의 xml 설정 대신 Java의 애너테이션 기반의 설정을 적극적으로 사용하고 있기 때문에 무겁고 작성하기 힘들던 xml 대신에 애너테이션을 사용하여 아주 간편하게 설정할 수 있습니다.
    • 기본적으로 개발에 필요한 설정 정보들을 일반적으로 많이 사용하는 설정 값을 default로 하여 자동으로 설정해주고 있습니다.
  • 또한 외부 라이브러리나 하위 프레임워크들의 의존성 관리가 매우 쉬워졌습니다.
    • 기존에는 외부 라이브러리와 프레임워크를 사용하기 위해서 각각의 버전들의 호환성을 직접 확인해 가면서 의존성들을 설정해야 했지만, SpringBoot에서는 spring-boot-starter-web처럼 필요한 외부 라이브러리들과 프레임워크들을 의존성에 맞게 starter로 묶어서 제공해 줍니다.
    • 따라서 이전처럼 각각의 버전 호환성을 직접 확인할 필요가 없어졌습니다.
  • 마지막으로 SpringBoot의 강력한 점을 물어봤을 때 대답하는 것 중 하나가 바로 내장 Apache Tomcat입니다.
    • Spring 프레임워크에서는 서버를 실행시키기 위해 Apache Tomcat을 직접 다운로드하고 설정하고 프로젝트에 삽입했어야 했습니다.
    • 이러한 불편함을 해결하기 위해 SpringBoot에서는 기본적으로 starter-web dependency를 설정하면 자동으로 내장형 Apache Tomcat을 제공해 줍니다.
    • 말 그대로 Apache Tomcat이 내장되어 있기 때문에 개발자가 따로 다운로드하고 설정하고 삽입할 필요 없이 바로 사용할 수 있게 되었습니다.

 

Postman

  • Postman이란 API 개발을 빠르고 쉽게 구현할 수 있도록 도와주는 소프트웨어 플랫폼입니다.
  • API는 하나의 "약속"이라 배웠습니다.
    • 우리가 API 즉, 약속에 맞춰서 HTTP 요청을 서버에 보내고 응답을 확인해 봐야 우리가 만든 서버가 제대로 동작하는지 확인할 수 있습니다.
  • 이러한 확인 작업을 간편하게 할 수 있도록 도와주는 플랫폼 중 하나가 Postman입니다.

 

HTTP

  • HTTP(HyperText Transfer Protocol)란?
    • 데이터를 주고받는 양식을 정의한 "통신 규약"중 하나가 HTTP입니다.
    • 매우 범용적인 양식을 가지고 있어 전 세계에서 제일 널리 쓰이는 통신 규약입니다.
    • 여기서 말하는 통신 규약이란, 컴퓨터끼리 데이터를 주고받을 때 정해둔 약속을 의미합니다.
    • 현재 이용되는 대부분의 웹 서버가 HTTP를 기반으로 정해준 규칙에 맞게 데이터를 주고받습니다.
    • 또한, 모든 브라우저는 HTTP 프로토콜을 기본으로 지원하기 때문에 여러분은 매일 HTTP를 이용하는 셈이 됩니다.
  • 우리는 어떻게 HTTP로 데이터를 주고받을까?
    • HTTP에서는 언제나 Request, Response라는 개념이 존재합니다.
    • 서버와 브라우저의 관계로 가볍게 말해보면 아래와 같이 동작합니다.
      1. 브라우저는 서버에게 자신이 원하는 페이지(URL 등의 정보)를 요구(Request)합니다.
      2. 서버는 브라우저가 원하는 페이지가 있는지 확인하고, 있다면 해당 페이지에 대한 데이터를 실어 응답(Response)해줍니다. 없다면 없는 페이지에 대한 데이터를 반환합니다.
      3. 브라우저는 서버에게 전달받은 데이터를 기반으로 브라우저에 그려줍니다.
    • 그리고 위와 같은 사례에서 "데이터"는 어떠한 데이터든 주고받는 게 가능합니다.

 

HTTP에는 크게 다음과 같은 구성 요소가 존재합니다.
  •  
  • Method (호출/요청 방식)
    • GET: 이름 그대로 어떤 리소스를 얻을 때 사용됩니다. 브라우저의 주소창에 URL을 입력하면 GET 메서드를 사용해서 서버에 요청을 보냅니다.
    • POST: 웹 서버에 데이터를 게시할 때 사용하는 게 일반적입니다. (ex. 회원가입, 게시글 작성, 댓글 작성)
    • 그 외 DELETE 등의 여러 요청 방식이 존재합니다.
    • 가장 대표적인 요청 방식이 GET과 POST입니다.
  • Header (추가 데이터. 메타 데이터)
    • 브라우저가 어떤 페이지를 원하는지
    • 요청받은 페이지를 찾았는지
    • 요청받은 데이터를 성공적으로 찾았는지
    • 어떤 형식으로 데이터를 보낼지
GET naver.com HTTP/1.1
이러한 사례 외에도 아주 다양한 의사 표현을 위한 데이터를 모두 Header 필드에 넣고 주고받습니다. 위에서 설명된 메서드도 사실은 헤더에 포함되어 서버로 보내집니다.
  • Payload (데이터. 실제 데이터)
    • 서버가 응답을 보낼 때에는 항상 Payload를 보낼 수 있습니다.
    • 클라이언트(브라우저)가 요청을 할 때에도 Payload를 보낼 수 있습니다.
    • 리고 "GET method를 제외하곤 모두 Payload를 보낼 수 있다"는 게 HTTP에서의 약속입니다.

 

테스트 코드 

개발 코드 배포 전, 버그를 (최대한 많이) 찾아내는 법 - 테스트!
  • 블랙박스 테스팅: 소프트웨어 내부 구조나 동작원리를 모르는 블랙박스와 같은 상태에서, 즉 웹 서비스의 사용자 입장에서 동작을 검사하는 방법
    1. 장점
      • 누구나 테스트가 가능합니다 - 개발자부터 디자이너, 베타테스터 혹은 사장님까지!
    2. 단점
      • 기능이 증가될수록 테스트의 범위가 증가합니다. (-> 시간이 갈수록 테스트하는 사람이 계속 늘어나야 함) 
      • 테스트하는 사람에 따라 테스트 퀄리티가 다를 수 있습니다. → QA 직군이 있는 이유
  • 개발자 테스트: 개발자가 직접 "본인이 작성한 코드"를 검증하기 위해 "테스트 코드"를 작성
    1. 장점
      • 빠르고 정확한 테스트가 가능합니다. (예상 동작 VS 실제 동작)
      • 테스트 자동화가 가능합니다. (-> 배포 절차 시 테스트 코드가 수행되어 동작 검증)
      • 리팩토링이나 기능 추가를 할 때 더욱 편리합니다.
    2. 단점
      • 개발 시간이 오래 걸림
      • 테스트 코드를 유지보수하는 비용

 

📌 Spring에서는 '테스트 코드' 작성을 잘할 수 있는 환경을 제공해 줍니다.

JUnit

  • JUnit이란 자바 프로그래밍 언어 용 단위 테스트 프레임워크입니다.

build.gradle 파일을 열어보면 JUnit 사용을 위한 환경설정이 이미 되어 있습니다.

 

Lombok과 application.properties

  • Lombok
    • Lombok(이하 롬복)은, 자바 프로젝트를 진행하는데 거의 필수적으로 필요한 메서드/생성자 등을 자동 생성해 줌으로써 코드를 절약할 수 있도록 도와주는 라이브러리입니다.
  • application.properties
    • application.properties는 Spring과 관련된 설정을 할 때 사용되는 파일입니다.
    • Spring과 SpringBoot의 차이에 대해 학습할 때 SpringBoot를 사용하면 개발에 필요한 설정 정보들이 자동으로 설정된다고 배웠습니다.
    • 이 파일을 사용하면 자동으로 설정되고 있는 설정 값을 쉽게 수정할 수 있습니다.
    • 뿐만 아니라 DB 연결 시 DB의 정보를 제공해야 하는데 이러한 경우에도 이 파일을 이용하여 쉽게 값을 전달할 수 있습니다.
    • pache Tomcat을 사용하여 서버를 실행하면 기본 port 설정이 8080으로 되어있습니다.
      • application.properties 파일에서 server.port=8081 이렇게 설정을 하면 서버의 port 번호를 ‘8080’에서 ‘8081’로 바꿔서 실행시킬 수 있습니다.

 

Spring MVC

MVC 디자인 패턴

  • MVCModel-View-Controller의 약자로, 소프트웨어 디자인 패턴 중 하나입니다.
    • 디자인 패턴 → 어떠한 효율적인 방법들을 패턴화 해놓은 것.
  • MVC 패턴소프트웨어를 구성하는 요소들을 Model, View, Controller로 구분하여 각각의 역할을 분리합니다.
📌 MVC 패턴은 소프트웨어를 구성하는 요소들을 분리함으로써 코드의 재사용성과 유지보수성을 높이고, 개발자들 간의 협업을 용이하게 합니다. 따라서 소프트웨어를 개발할 때, MVC 패턴을 적용하여 구조를 잘 설계하는 것이 중요합니다.

 

Model

  • 데이터와 비즈니스 로직을 담당합니다.
  • 데이터베이스와 연동하여 데이터를 저장하고 불러오는 등의 작업을 수행합니다.

View

  • 사용자 인터페이스를 담당합니다.
  • 사용자가 보는 화면과 버튼, 폼 등을 디자인하고 구현합니다.

Controller

  • Model과 View 사이의 상호작용을 조정하고 제어합니다.
  • 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트합니다.

 

Spring MVC

Spring Web MVC는 Servlet API를 기반으로 구축된 독창적인 웹 프레임워크로, 처음부터 Spring Framework에 포함되어 왔으며, 정식 명칭인 "Spring Web MVC"는 소스 모듈(spring-webmvc)의 이름에서 따왔으나, "Spring MVC"로 더 일반적으로 알려져 있습니다.

Spring MVC는 중앙에 있는 DispatcherServlet이 요청을 처리하기 위한 공유 알고리즘을 제공하는 Front Controller 패턴을 중심으로 설계되어 있으며 이 모델은 유연하고 다양한 워크 플로우를 지원합니다.
https://docs.spring.io/spring-framework/reference/web/webmvc.html
  • Spring 공식 문서에서 Spring MVC에 대한 설명으로 DispatcherServlet이 중앙에서 HTTP 요청을 처리해 주는데 이는 Front Controller 패턴으로 설계되어 있다’라고 설명하고 있습니다.
  • 쉽게 표현해 보자면 ‘Spring에서 MVC 디자인 패턴을 적용하여 HTTP 요청을 효율적으로 처리하고 있다’라고 이해하시면 좋습니다.
DispatcherServlet에 대한 이해를 돕기 위해 먼저 Servlet이 무엇인지 학습하고 가겠습니다.

Servlet

  • Servlet(서블릿)은 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램 혹은 그 사양을 말합니다.

사용자가 (HTTP) API 요청했을 때 서버의 서블릿이 어떻게 동작되고 있는지

  1. 사용자가 Client(브라우저)를 통해 서버에 HTTP Request 즉, API 요청을 합니다.
  2. 요청을 받은 Servlet 컨테이너HttpServletRequest, HttpServletResponse 객체를 생성합니다.
    • 약속된 HTTP의 규격을 맞추면서 쉽게 HTTP에 담긴 데이터를 사용하기 위한 객체입니다.
  3. 설정된 정보를 통해 어떠한 Servlet에 대한 요청인지 찾습니다.
  4. 해당 Servlet에서 service 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet 혹은 doPost 등의 메서드를 호출합니다.
  5. 호출한 메서드들의 결과를 그대로 반환하거나 동적 페이지를 생성한 뒤 HttpServletResponse 객체에 응답을 담아 Client(브라우저)에 반환합니다.
  6. 응답이 완료되면 생성한 HttpServletRequest, HttpServletResponse 객체를 소멸합니다.

☕️ 서블릿(Servlet) 한눈에 이해하기

즉, 서블릿은 "웹 서버 안에서 일하는 똑똑한 비서"라고 보시면 됩니다.
  • 서블릿은 자바를 이용해 웹 요청을 처리하는 작은 프로그램입니다.
    • 쉽게 말하자면: 클라이언트(손님)가 주문(HTTP 요청)을 하면, 주방(서버)에서 요리를 해서 내어주는 요리사 역할을 합니다.
  • 서블릿의 동작 과정 (비유로 이해하기)
    • 사용자가 브라우저에서 버튼을 누르는 순간부터 응답을 받기까지의 과정입니다.
단계 과정 (공식 명칭) 쉬운 설명 (비유)
1단계 HTTP Request 손님이 식당에 들어와서 "제육덮밥 하나 주세요!"라고 주문서를 냅니다.
2단계 객체 생성 지배인(서블릿 컨테이너)이 주문을 받자마자 주문서(Request)빈 접시(Response)를 준비합니다.
3단계 서블릿 찾기 이 지배인이 "제육덮밥 담당 요리사(해당 서블릿)가 누구지?" 하고 담당자를 찾습니다.
4단계 service 메서드 호출 요리사가 주방으로 불려 나옵니다. 이때 어떤 요리법(GET, POST 등)을 쓸지 결정합니다.
5단계 doGet / doPost 실행 요리사가 실제로 요리를 만듭니다. (데이터베이스를 뒤지거나 로직을 처리함)
6단계 응답 반환 완성된 요리를 아까 준비한 접시(Response)에 담아서 손님에게 보냅니다.
7단계 객체 소멸 식사가 끝났으니 주문서와 빈 접시는 쓰레기통에 버립니다. (메모리 정리)
  • 핵심 키워드 정리
    • 서블릿 컨테이너 (Servlet Container): 서블릿들을 관리하는 관리자입니다. (대표적으로 'Tomcat'이 있습니다.)
    • HttpServletRequest: 클라이언트가 보낸 정보(아이디, 비번, 주소 등)가 담긴 우편물입니다.
    • HttpServletResponse: 서버가 클라이언트에게 줄 결과물을 담는 빈 상자입니다.
    • doGet / doPost: 클라이언트가 데이터를 단순히 달라고 하는지(GET), 아니면 저장해 달라고 보내는지(POST)에 따라 구분해서 처리하는 방법입니다.
💡 요약 
  • "서블릿은 컨테이너라는 관리자 아래에서, 손님의 요청(Request)을 받아 요리를 하고 응답(Response)을 돌려주는 자바 요리사이다!"
    • 이 흐름을 이해하셨다면, DispatcherServlet은 "모든 요리 주문을 일단 혼자 다 받아서 적절한 요리사에게 나눠주는 총지배인"이라고 생각하시면 훨씬 쉬워질 거예요!

 

Front Controller

  • 모든 API 요청을 앞서 살펴본 서블릿의 동작 방식에 맞춰 코드를 구현한다면 무수히 많은 Servlet 클래스를 구현해야 합니다.
  • 따라서 Spring은 DispatcherServlet을 사용하여 Front Controller 패턴 방식으로 API 요청을 효율적으로 처리하고 있습니다.
"전담 매니저(DispatcherServlet) 등장"
  • 이제는 무수히 많은 서블릿 대신, 딱 하나의 거대한 서블릿이 입구에서 모든 요청을 다 받습니다.
  • 이를 Front Controller(프런트 컨트롤러) 패턴이라고 부릅니다.
    1. 입구(DispatcherServlet): 모든 API 요청을 혼자 다 받습니다. ("어떤 요청이든 일단 나한테 와!")
    2. 안내원: 요청 주소를 보고 "아, 이건 로그인 담당한테 보내야겠네?" 하고 판단합니다.
    3. 일반 클래스(Controller): 서블릿이 아닌 그냥 일반 자바 클래스(우리가 흔히 만드는 @Controller)에게 일을 시킵니다.
  • 그래서 서블릿 자체는 무수히 많을 수 있지만, 요즘 우리가 배우는 Spring 환경에서는 DispatcherServlet이라는 강력한 녀석 하나가 대장 노릇을 하고 있다고 보시면 됩니다!

Front Controller 패턴의 동작 과정

 

1️⃣ Client(브라우저)에서 HTTP 요청이 들어오면 DispatcherServlet 객체가 요청을 분석합니다.

2️⃣DispatcherServlet 객체는 분석한 데이터를 토대로 Handler mapping을 통해 Controller를 찾아 요청을 전달해 줍니다.

 💡 [Sample]
GET /api/hello → HelloController의 hello() 함수
GET /user/login → UserController의 login() 함수
GET /user/signup → UserController의 signup() 함수
POST /user/signup → UserController의 registerUser() 함수
  • Handler mapping에는 API path와 Controller 메서드가 매칭되어 있습니다.
@RestController
public class HelloController {
    @GetMapping("/api/hello")
    public String hello() {
        return "Hello World!";
    }
}
  • API path 즉, URL을 Controller에 작성하는 방법은 @Controller 애너테이션이 달려있는 클래스를 생성한 뒤 @GetMapping처럼 요청한 HTTP Method와 일치하는 애너테이션을 추가한 메서드를 구현합니다.
    • URL은 @GetMapping("/api/hello") 이처럼 해당 애너테이션의 속성값으로 전달해 주면 됩니다.
    • 해당 메서드명은 URL을 매핑하는데 영향을 미치지 않음으로 자유롭게 정해도 상관없습니다.
  • 이제는 직접 Servlet을 구현하지 않아도 DispatcherServlet에 의해 간편하게 HTTP 요청을 처리할 수 있게 되었습니다.

 

3️⃣ ControllerDispathcerServlet

  • 해당 Controller는 요청에 대한 처리를 완료 후 처리에 대한 결과 즉, 데이터('Model')와 'View' 정보를 전달합니다.

4️⃣DispatcherServletClient

  • ViewResolver 통해 View에 Model을 적용하여 View를 Client에게 응답으로 전달합니다

 

Controller 

  • Spring MVC는 효율적인 API 처리를 위해 Front Controller 패턴을 만들어냈습니다.
  • 이제는 API 마다 파일을 만들 필요 없습니다.
    • 보통 하나의 Contoller에 모든 API를 넣지는 않습니다.
    • 유사한 성격의 API를 하나의 Controller로 관리합니다.
  • 메서드 이름도 내 마음대로 설정 가능합니다. (단, 클래스 내의 중복메서드명 불가)
package com.sparta.springmvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/api")
public class HelloController {
    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "Hello World";
    }

    @GetMapping("/get")
    @ResponseBody
    public String get() {
        return "GET Method 요청";
    }

    @PostMapping("/post")
    @ResponseBody
    public String post() {
        return "POST Method 요청";
    }

    @PutMapping("/put")
    @ResponseBody
    public String put() {
        return "PUT Method 요청";
    }

    @DeleteMapping("/delete")
    @ResponseBody
    public String delete() {
        return "DELETE Method 요청";
    }
}

 

  • @GET, @POST, @PUT, @DELETE
    • 각각의 HTTP Method에 매핑되는 애너테이션입니다.
  • @RequestMapping은 중복되는 URL를 단축시켜 줄 수 있습니다.

 

정적 페이지와 동적 페이지

 

정적 페이지 처리하기

 

1. static 폴더

🌐 http://localhost:8080/hello.html

  • SpringBoot 서버에 html 파일을 바로 요청하면 해당 html 파일을 static 폴더에서 찾아서 반환해 줍니다.
  • 그렇다면 Controller를 거쳐서 html을 반환할 수도 있을까요?
    • 물론 가능합니다. 하지만 이미 완성된 정적인 html 파일을 Controller를 통해서 반환할 필요는 없겠죠?
    • Controller를 통해서 반환하는 것을 테스트하려면 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf’ 해당 dependency를 주석 처리해야 테스트가 가능합니다.
  • Thymeleaf 동적 페이지 처리를 위한 템플릿 엔진입니다. 추가하면 자동으로 Controller에서 html 파일 찾는 경로를/resources/templates로 설정합니다.
@GetMapping("/static-hello")
public String hello() {
    return "hello.html";
}
주석 처리한 후 "hello.html" 이렇게 문자열로 반환하면 static 폴더의 해당 html 파일을 찾아 반환해 줍니다.

 

2. Redirect

🌐 http://localhost:8080/html/redirect

@GetMapping("/html/redirect")
public String htmlStatic() {
    return "redirect:/hello.html";
}
  • 템플릿 엔진을 적용한 상태에서 static 폴더의 html 파일을 Controller를 통해서 처리하고 싶다면
    • 이렇게 "redirect:/hello.html" redirect 요청을 문자열로 반환하면 http://localhost:8080/hello.html 요청이 재수행되면서 static 폴더의 파일을 반환할 수 있습니다.

 

3. Template engine에 View 전달

🌐 http://localhost:8080/html/templates

@GetMapping("/html/templates")
public String htmlTemplates() {
    return "hello";
}
  • 타임리프 default 설정
    • prefix: classpath:/templates/
    • suffix: .html

  • static 폴더에 있는 html 파일을 바로 호출하는 방법이 가장 간단하지만
  • 외부 즉, 브라우저에서 바로 접근하지 못하게 하고 싶거나 특정 상황에 Controller를 통해서 제어하고 싶다면
  • 이렇게 templates 폴더에 해당 정적 html 파일을 추가하고 해당 html 파일명인 "hello" 문자열을 반환하여 처리할 수 있습니다. (.html은 생략 가능!)

 

동적 페이지 처리하기

🌐 http://localhost:8080/html/dynamic

private static long visitCount = 0;

...

@GetMapping("/html/dynamic")
public String htmlDynamic(Model model) {
    visitCount++;
    model.addAttribute("visits", visitCount);
    return "hello-visit";
}

 

  • 동적 페이지 처리 과정
    1. Client의 요청을 Controller에서 Model로 처리합니다.
      • DB 조회가 필요하다면 DB 작업 후 처리한 데이터를 Model에 저장합니다.
    2. Template engine(Thymeleaf) 에게 View, Model 전달합니다.
      • View: 동적 HTML 파일
      • Model: View에 적용할 정보들
    3. Template engine
      1. ViewModel을 적용 → 동적 웹페이지 생성
        1. 예) 로그인 성공 시, "로그인된 사용자의 Nickname"을 페이지에 추가
        2. Template engine 종류: 타임리프(Thymeleaf), Groovy, FreeMarker, Jade, JSP 등
    4. Client(브라우저)에게 View(동적 웹 페이지, HTML)를 전달해 줍니다.

 

데이터를 Client에 반환하는 방법

Response 트렌드의 변화

그렇다면 서버는 요청을 받아 html/css/js 파일을 반환해주는게 주 업무일까요?

  • 당연히 정답은 없지만, 최근의 경향으로는 그렇지는 않습니다. 예전에는 조금 더 그랬었던 편이지만, 웹 생태계가 고도화되는 과정 중에 상대적으로 프런트엔드와 백엔드가 각각 따로 발전하게 되면서, 느슨하게 결합하는 방식을 더 많이 채택하게 되었고, 최근에는 서버가 직접 뷰(html/css/js)를 반환하기보다는 요청에 맞는 특정한 정보만 반환하는 것을 조금 더 선호하기도 합니다. 그래서 요즘에는 주로 서버에서는 데이터 교환 포맷 중 JSON 형태로 데이터를 반환하기도 하는데, 보통 이렇게 생겼습니다!

 

JSON 데이터 반환하는 방법

  • 템플릿 엔진이 적용된 SpringBoot에서는 Controller에서 문자열을 반환하면 templates 폴더에서 해당 문자열의 .html 파일을 찾아서 반환해 줍니다.
  • 따라서 html 파일이 아닌 JSON 데이터를 브라우저에 반환하고 싶다면 해당 메서드에 @ResponseBody 애너테이션을 추가해줘야 합니다.

 

1. 반환값: String

🌐 http://localhost:8080/response/json/string

@GetMapping("/response/json/string")
@ResponseBody
public String helloStringJson() {
    return "{\"name\":\"Robbie\",\"age\":95}";
}
  • Java는 JSON 타입을 지원하지 않기 때문에 JSON 형태의 String 타입으로 변환해서 사용해야 합니다.

 

2. 반환값: String 외 자바 클래스

🌐 http://localhost:8080/response/json/class

@GetMapping("/response/json/class")
@ResponseBody
public Star helloClassJson() {
    return new Star("Robbie", 95);
}
  • "자바 객체 → JSON으로 변환"
  • Spring에서 자동으로 Java의 객체를 JSON으로 변환해 줍니다.

 

@RestController

= @Controller + @ResponseBody

  • @RestController를 사용하면 해당 클래스의 모든 메서드에 @ResponseBody 애너테이션이 추가되는 효과를 부여할 수 있습니다.

 

Jackson 라이브러리 

  • JacksonJSON 데이터 구조를 처리해 주는 라이브러리입니다.
    • Object를 JSON 타입의 String으로 변환해 줄 수 있습니다.
    • JSON 타입의 StringObject로 변환해 줄 수 있습니다.
  • Spring은 3.0 버전 이후로 Jacskon과 관련된 API를 제공함으로써, 우리가 직접 소스 코드를 작성하여 JSON 데이터를 처리하지 않아도 자동으로 처리해주고 있습니다.
    • 따라서 SpringBoot의 starter-web에서는 default로 Jackson 관련 라이브러리들을 제공하고 있습니다.
    • 직접 JSON 데이터를 처리해야 할 때는 Jackson 라이브러리의 ObjectMapper를 사용할 수 있습니다.

 

Object To JSON

@Test
@DisplayName("Object To JSON : get Method 필요")
void test1() throws JsonProcessingException {
    Star star = new Star("Robbie", 95);

    ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper
    String json = objectMapper.writeValueAsString(star);

    System.out.println("json = " + json);
}
  • objectMapperwriteValueAsString 메서드를 사용하여 변환할 수 있습니다.
    • 파라미터에 JSON으로 변환시킬 Object의 객체를 주면 됩니다.
  • Object를 JSON 타입의 String으로 변환하기 위해서는 해당 Object에 get Method가 필요합니다.

 

JSON To Object

@Test
@DisplayName("JSON To Object : 기본 생성자 & (get OR set) Method 필요")
void test2() throws JsonProcessingException {
    String json = "{\"name\":\"Robbie\",\"age\":95}"; // JSON 타입의 String

    ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper

    Star star = objectMapper.readValue(json, Star.class);
    System.out.println("star.getName() = " + star.getName());
}
  • objectMapper의 readValue 메서드를 사용하여 변환할 수 있습니다.
    • 첫 번째 파라미터는 JSON 타입의 String, 두 번째 파라미터에는 변환할 Object의 class 타입을 주면 됩니다.
  • JSON 타입의 StringObject로 변환하기 위해서는 해당 Object에 기본 생성자get 혹은 set 메서드가 필요합니다.

 

Path Variable과 Request Param

 

Path Variable 

  • Client 즉, 브라우저에서 서버로 HTTP 요청을 보낼 때 데이터를 함께 보낼 수 있습니다.
  • 서버에서는 이 데이터를 받아서 사용해야 하는데 데이터를 보내는 방식이 한 가지가 아니라 여러 가지가 있기 때문에 모든 방식에 대한 처리 방법을 학습해야 합니다.
Path Variable 방식

🌐 GET http://localhost:8080/hello/request/star/Robbie/age/95

  • 서버에 보내려는 데이터를 URL 경로에 추가할 수 있습니다.
  • /star/Robbie/age/95
    • ‘Robbie’와 ‘95’ 데이터를 서버에 보내기 위해 URL 경로에 추가했습니다.
// [Request sample]
// GET http://localhost:8080/hello/request/star/Robbie/age/95
@GetMapping("/star/{name}/age/{age}")
@ResponseBody
public String helloRequestPath(@PathVariable String name, @PathVariable int age)
{
    return String.format("Hello, @PathVariable.<br> name = %s, age = %d", name, age);
}
  • 데이터를 받기 위해서는 /star/{name}/age/{age} 이처럼 URL 경로에서 데이터를 받고자 하는 위치의 경로{data} 중괄호를 사용합니다.
  • (@PathVariable String name, @PathVariable int age)
    • 그리고 해당 요청 메서드 파라미터에 @PathVariable 애너테이션과 함께 {name} 중괄호에 선언한 변수명과 변수타입을 선언하면 해당 경로의 데이터를 받아올 수 있습니다.

 

Request Param

Request Param 방식

🌐 GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95

  • 서버에 보내려는 데이터를 URL 경로 마지막에 ?& 를 사용하여 추가할 수 있습니다.
  • ?name=Robbie&age=95
    • ‘Robbie’와 ‘95’ 데이터를 서버에 보내기 위해 URL 경로 마지막에 추가했습니다.
// [Request sample]
// GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
@GetMapping("/form/param")
@ResponseBody
public String helloGetRequestParam(@RequestParam String name, @RequestParam int age) {
    return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
  • 데이터를 받기 위해서는 ?name=Robbie&age=95 에서 key 부분에 선언한 name과 age를 사용하여 value에 선언된 Robbie, 95 데이터를 받아올 수 있습니다.
  • (@RequestParam String name, @RequestParam int age)
    • 해당 요청 메서드 파라미터에 @RequestParam 애너테이션과 함께 key 부분에 선언한 변수명과 변수타입을 선언하면 데이터를 받아올 수 있습니다.

 

form 태그 POST

🌐 POST http://localhost:8080/hello/request/form/param

<form method="POST" action="/hello/request/form/model">
  <div>
    이름: <input name="name" type="text">
  </div>
  <div>
    나이: <input name="age" type="text">
  </div>
  <button>전송</button>
</form>
  • HTML의 form 태그를 사용하여 POST 방식으로 HTTP 요청을 보낼 수 있습니다.
  • 이때 해당 데이터는 HTTP Body에 name=Robbie&age=95 형태로 담겨서 서버로 전달됩니다.
// [Request sample]
// POST http://localhost:8080/hello/request/form/param
// Header
//  Content type: application/x-www-form-urlencoded
// Body
//  name=Robbie&age=95
@PostMapping("/form/param")
@ResponseBody
public String helloPostRequestParam(@RequestParam String name, @RequestParam int age) {
    return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
  • 해당 데이터를 받는 방법은 앞서 본 방법처럼 @RequestParam 애너테이션을 사용하여 받아올 수 있습니다.

 

// [Request sample]
// GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
@GetMapping("/form/param")
@ResponseBody
public String helloGetRequestParam(@RequestParam(required = false) String name, int age) {
    return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
  • @RequestParam은 생략이 가능합니다.
  • @RequestParam(required = false)
    • 이렇게 required 옵션false로 설정하면 Client에서 전달받은 값들에서 해당 값이 포함되어있지 않아도 오류가 발생하지 않습니다.
    • @PathVariable(required = false) 도 해당 옵션이 존재합니다.
    • Client로부터 값을 전달받지 못한 해당 변수는 null로 초기화됩니다.

 

HTTP 데이터를 객체로 처리하는 방법

@ModelAttribute

form 태그 POST

🌐 POST http://localhost:8080/hello/request/form/model

// [Request sample]
// POST http://localhost:8080/hello/request/form/model
// Header
//  Content type: application/x-www-form-urlencoded
// Body
//  name=Robbie&age=95
@PostMapping("/form/model")
@ResponseBody
public String helloRequestBodyForm(@ModelAttribute Star star) {
    return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • HTML의 form 태그를 사용하여 POST 방식으로 HTTP 요청을 보낼 수 있습니다.
  • 이때 해당 데이터는 HTTP Body에 name=Robbie&age=95 형태로 담겨서 서버로 전달됩니다.
  • 해당 데이터를 Java의 객체 형태로 받는 방법@ModelAttribute 애너테이션을 사용한 후 Body 데이터를 Star star 받아올 객체를 선언합니다.

 

Query String 방식

🌐 GET http://localhost:8080/hello/request/form/param/model?name=Robbie&age=95

 

// [Request sample]
// GET http://localhost:8080/hello/request/form/param/model?name=Robbie&age=95
@GetMapping("/form/param/model")
@ResponseBody
public String helloRequestParam(@ModelAttribute Star star) {
    return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • ?name=Robbie&age=95 처럼 데이터가 두 개만 있다면 괜찮지만 여러 개 있다면 @RequestParam 애너테이션으로 하나씩 받아오기 힘들 수 있습니다.
  • 이때 @ModelAttribute 애너테이션을 사용하면 Java의 객체로 데이터를 받아올 수 있습니다.
    • 파라미터에 선언한 Star 객체가 생성되고, 오버로딩된 생성자 혹은 Setter 메서드를 통해 요청된 name & age의 값이 담깁니다.

 

⚠️ @ModelAttribute는 생략이 가능합니다.
  • 이때, 생각해 볼 문제가 있습니다! Spring에서는 @ModelAttribute뿐만 아니라 @RequestParam도 생략이 가능합니다.
  • 그렇다면 Spring은 이를 어떻게 구분할까요?
    • 간단하게 설명하자면 Spring은 해당 파라미터(매개변수)가 SimpleValueType이라면 @RequestParam으로 간주하고
    • 아니라면 @ModelAttribute가 생략되어 있다 판단합니다.
    • SimpleValueType은 원시타입(int), Wrapper타입(Integer), Date 등의 타입을 의미합니다.

 

@RequestBody

  • HTTP BodyJSON 데이터를 담아 서버에 전달할 때 해당 Body 데이터Java의 객체로 전달받을 수 있습니다.

🌐 POST http://localhost:8080/hello/request/form/json

// [Request sample]
// POST http://localhost:8080/hello/request/form/json
// Header
//  Content type: application/json
// Body
//  {"name":"Robbie","age":"95"}
@PostMapping("/form/json")
@ResponseBody
public String helloPostRequestJson(@RequestBody Star star) {
    return String.format("Hello, @RequestBody.<br> (name = %s, age = %d) ", star.name, star.age);
}
  • HTTP Body에 {"name":"Robbie","age":"95"}  JSON 형태로 데이터가 서버에 전달되었을 때 @RequestBody 애너테이션을 사용해 데이터를 객체 형태로 받을 수 있습니다.

 

⚠️ 데이터를 Java의 객체로 받아올 때 주의할 점이 있습니다.
  • 해당 객체의 필드에 데이터를 넣어주기 위해 set or get 메서드 또는 오버로딩된 생성자가 필요합니다.
  • 예를 들어 @ModelAttribute 사용하여 데이터를 받아올 때 해당 객체에 set 메서드 혹은 오버로딩된 생성자가 없다면 받아온 데이터를 해당 객체의 필드에 담을 수 없습니다.
  • 이처럼 객체로 데이터를 받아올 때 데이터가 제대로 들어오지 않는다면 우선 해당 객체의 set or get 메서드 또는 오버로딩된 생성자의 유무를 확인하시면 좋습니다. 

 

DTO

  • 이름에서도 알 수 있듯이 DTO(Data Transfer Object)데이터 전송 및 이동을 위해 생성되는 객체를 의미합니다.
  • Client에서 보내오는 데이터를 객체로 처리할 때 사용됩니다.
  • 또한 서버의 계층 간의 이동에도 사용됩니다.
  • 그리고 DB와의 소통을 담당하는 Java 클래스를 그대로 Client에 반환하는 것이 아니라 DTO로 한번 변환한 후 반환할 때도 사용됩니다.

 

Database

  • Database를 한 마디로 정의하면 ‘데이터의 집합’이라고 할 수 있습니다.
  • DB는 우리가 매일 사용하는 카톡 메시지, 인스타그램의 사진등의 정보를 저장하고 관리해 줍니다.

 

💡 DBMS

  • DBMS는 ‘Database Management System’의 약자로 Database를 관리하고 운영하는 소프트웨어를 의미합니다. 

💡 RDBMS

  • RDBMS는 ‘Relational DBMS’의 약자로 관계형 데이터베이스라고 불립니다.
  • RDBMS는 테이블(table)이라는 최소 단위로 구성되며, 이 테이블은 열(column)과 행(row)으로 이루어져 있습니다.
  • 테이블 간 FK(Foreign Key)를 통해 다른 데이터를 조합해서 함께 볼 수 있다는 장점이 있습니다.

💡 MySQL

  • MySQL은 우리가 서비스를 배포할 때 사용할 데이터베이스입니다.
  • AWS RDS라는 서비스를 사용해 붙여볼 예정입니다.
  • Spring과 궁합이 좋아서 많은 회사에서 사용하고 있습니다.

 

JDBC

📌 애플리케이션 서버와 데이터베이스는 어떻게 소통해야 할까요?

  • 우리는 애플리케이션 서버에서 요청을 받고, 해당 요청을 처리하기 위해 데이터베이스와 소통을 해야 합니다.
  • 따라서 실제로 서버가 데이터베이스와 어떠한 방법을 통해 소통하고 있는지 알아보려고 합니다. 

 

JDBC의 등장배경

  • 애플리케이션 서버에서 DB에 접근하기 위해서는 여러 가지 작업이 필요합니다.
    1. 우선 DB에 연결하기 위해 커넥션을 연결해야 합니다.
    2. SQL을 작성한 후 커넥션을 통해 SQL을 요청합니다.
    3. 요청한 SQL에 대한 결과를 응답받습니다.

 

  • 기존에 사용하던 MySQL 서버를 PostgreSQL 서버로 변경한다면 무슨 일이 발생할까요?
  • MySQL과 PostgreSQL은 커넥션을 연결하는 방법, SQL을 전달하는 방법, 결과를 응답받는 방법 모두 다를 수 있습니다.
  • 따라서 애플리케이션 서버에서 작성했던 DB 연결 로직들을 전부 수정해야 합니다.

 

  • 이러한 문제를 해결하기 위해 JDBC 표준 인터페이스가 등장했습니다.
  • JDBC는 Java Database Connectivity로 DB에 접근할 수 있도록 Java에서 제공하는 API입니다.
  • JDBC에 연결해야 하는 DB의 JDBC 드라이버를 제공하면 DB 연결 로직을 변경할 필요 없이 DB 변경이 가능합니다.
    • DB 회사들은 자신들의 DB에 맞도록 JDBC 인터페이스를 구현한 후 라이브러리로 제공하는데 이를 JDBC 드라이버라 부릅니다.
  • 따라서, MySQL 드라이버를 사용해 DB에 연결을 하다 PostgreSQL 서버로 변경이 필요할 때 드라이버만 교체하면 손쉽게 DB 변경이 가능합니다.

 

JdbcTemplate

  • DBC의 등장으로 손쉽게 DB교체가 가능해졌지만 아직도 DB에 연결하기 위해 여러 가지 작업 로직들을 직접 작성해야 한다는 불편함이 남았습니다.
  • 이러한 불편함을 해결하기 위해 커넥션 연결, statement 준비 및 실행, 커넥션 종료 등의 반복적이고 중복되는 작업들을 대신 처리해 주는 JdbcTemplate이 등장했습니다.

 

3 Layer Architecture

서버 개발자들은 서버에서의 처리과정이 대부분 비슷하다는 걸 깨닫고, 처리 과정을 크게 Controller, Service, Repository 3개로 분리했습니다. 

  • 사용자의 요청을 받는 '표현 계층(Controller)', 비즈니스 로직을 처리하는 '서비스 계층(Service)', 데이터베이스에 접근하는 '데이터 액세스 계층(Repository)'으로 나누어 관리하는 구조입니다.
  • 역할을 분리함으로써 코드의 재사용성을 높이고, 특정 계층에 문제가 생겨도 다른 계층에 영향을 주지 않아 유지보수가 쉬워집니다.

 

1. Controller

  • 클라이언트의 요청을 받습니다.
  • 요청에 대한 로직 처리Service에게 전담합니다.
    • Request 데이터가 있다면 Service에 같이 전달합니다.
  • Service에서 처리 완료된 결과를 클라이언트에게 응답합니다.

 

2. Service

 

  • 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세 중에 실세입니다.
    • 따라서 현업에서는 서비스 코드가 계속 비대해지고 있습니다.
  • DB 저장 및 조회가 필요할 때는 Repository에게 요청합니다.

 

3. Repository

 

  • DB 관리 (연결, 해제, 자원 관리) 합니다.
  • DB CRUD 작업을 처리합니다.

 

IoC(제어의 역전), DI(의존성 주입) 

Spring의 IoC와 DI

💡 요약

 

  • IoC (제어의 역전): 객체의 생성과 관리 권한을 개발자가 아닌 프레임워크가 가져가는 설계 원칙.
  • DI (의존성 주입): IoC를 실제로 구현하기 위해, 외부에서 객체를 주입해 주는 디자인 패턴.
  • 목적: 객체 간의 결합도를 낮춰 의존성을 최소화하고, 유지보수가 쉬운 '좋은 코드'를 만들기 위함.
  • Spring의 역할: IoC 컨테이너가 Bean(객체)을 알아서 관리해 주어 개발자가 비즈니스 로직에만 집중하게 돕는 밀키트.
    • IoC 컨테이너:"애플리케이션에 필요한 객체(Bean)들의 생성부터 소멸까지의 '생명주기'를 관리하고, 설정에 따라 '의존성 주입(DI)'을 수행하는 스프링의 핵심 엔진입니다."
      1. 관리 주체의 변화: 내가 직접 new 하던 걸 컨테이너가 대신 해줌 (제어의 역전).
      2. Bean(빈): 이 컨테이너가 관리하는 객체들을 '빈'이라고 불러요.
      3. ApplicationContext: 우리가 코드에서 실제로 마주하는 IoC 컨테이너의 구현체 이름이에요.
  • 한 줄 요약: "내가 직접 만들지 말고(IoC), 필요한 걸 밖에서 받아 쓰자(DI)!"

 

 

요리로 비교

  • IoCDI는 객체지향의 SOLID 원칙 그리고 GoF의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴입니다.
  • 이 둘을 더 자세하게 구분해 보자면 IoC는 설계 원칙에 해당하고 DI는 디자인 패턴에 해당합니다.

 

  • 좋은 코드를 위한 Spring의 IoC와 DI

  • Spring은 개발자가 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와주는 역할을 해줍니다.
  • 실생활에 비교해 보자면 요리도구까지 전부 들어있는 밀키트라고 할 수 있습니다.
  • 여기서 IoC와 DI는 좋은 코드 작성을 위한 Spring의 핵심 기술 중 하나라고 할 수 있습니다.
  • 아래는 Spring Docs에서 발췌해 온 내용입니다.

  • Spring의 핵심 기술을 소개하는 Docs에서 가장 처음으로 IoC 컨테이너에 대해서 설명하고 있습니다.
  • 그리고 IoC에 대해 ‘IoC는 DI로도 알려져 있다’라고 소개하고 있습니다.
  • 의역해보자면 ‘DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다’라고 이해하시면 좋을 것 같습니다.

 

의존성 

 

  • 의존성(Dependency): 한 객체가 자기 역할을 수행하기 위해 다른 객체를 필요로 하는 상태를 말합니다.
  • 강한 결합: 클래스 내부에서 직접 다른 객체를 생성(new)하는 것으로, 대상이 바뀌면 내 코드도 통째로 고쳐야 하는 위험한 상태입니다.
  • 약한 결합: 인터페이스를 사용해 추상적인 연결 고리만 만드는 것으로, 구체적인 종류가 바뀌어도 내 코드는 영향을 받지 않는 유연한 상태입니다.
  • 다형성의 활용: "치킨"이나 "피자"라는 실체 대신 "음식(Interface)"이라는 개념에 의존하면 어떤 메뉴가 들어와도 대응이 가능해집니다.
  • 한 줄 요약: "내가 직접 특정 물건을 사러 가지 말고, 어떤 물건이든 담을 수 있는 바구니(인터페이스)만 준비해 두는 것"이 핵심입니다.
이 '약한 결합' 상태를 스프링이 대신 만들어주는 것이 바로 앞에서 배운 DI예요!

 


주입

 

  • 주입(Injection): 필요한 객체를 스스로 만들지 않고, 외부에서 '전달(Injection)'받아 사용하는 행위입니다.
  • 필드 주입: 클래스의 변수(필드)에 객체를 직접 끼워 넣어주는 가장 단순한 방식입니다.
  • 메서드(Setter) 주입: 수정자 메서드를 통해 객체 생성 후 언제든 필요한 대상을 갈아 끼울 수 있는 방식입니다.
  • 생성자 주입: 객체가 태어날 때(생성 시점)부터 필요한 대상을 아예 들고 태어나게 하는 방식입니다. (Spring 권장)
  • 한 줄 요약: "직접 물건을 사러(new) 나가지 않고, 밖에서 배달 온 물건을 받아서(주입) 쓰기만 하는 것"입니다.
필요한 부품을 외부에서 조립해 준다고 생각하면 쉬워요. 이제 이 '주입'을 스프링이 대신해 주면 그게 바로 DI(의존성 주입)가 되는 거랍니다!  

 

제어의 역전(IoC)

 

  • 제어의 흐름 역전: 내가 직접 객체를 만들고 관리하던 권한을 외부(프레임워크)에 넘겨주는 설계 원칙입니다.
  • 기존 방식 (Consumer → Food): 먹고 싶은 음식을 내가 직접 요리(new) 해야 해서, 메뉴가 바뀔 때마다 내 일손이 늘어납니다.
  • IoC 방식 (Food → Consumer): 누군가 요리해서 가져다주는 음식을 나는 받기만 하면 되므로, 메뉴가 바뀌어도 나는 하던 일에만 집중할 수 있습니다.
  • 결과: 객체 간의 주도권이 바뀌면서 코드의 유연성이 높아지고, 변경 사항이 생겨도 내 코드를 수정할 일이 거의 없어집니다.
  • 한 줄 요약: "내가 직접 요리하지 말고, 차려진 밥상에 숟가락만 얹자!"

 

프레임워크(Framework)란?

  • 단어 그대로 해석하면 '뼈대'나 '틀'이라는 뜻이에요. 소프트웨어 개발에서는 "애플리케이션을 만들기 위한 구조와 기본 구성 요소가 이미 갖춰진 상태"를 의미합니다.
  • 비유: 밀키트보다는 조금 더 큰 개념인 '프랜차이즈 식당 시스템'이라고 생각하면 쉬워요.
    • 주방 기구, 인테리어, 주문 시스템(뼈대)은 본사가 이미 다 정해놨어요.
    • 주인은 그 틀 안에서 본사가 제공하는 레시피에 따라 재료를 넣고 조리(코드 작성)만 하면 됩니다.
  • 왜 '외부'라고 느껴질까?
    • 개발자가 작성하는 코드는 주로 '비즈니스 로직(핵심 기능)'입니다. 그런데 이 코드가 실제로 돌아가려면 서버를 띄우고, DB를 연결하고, 메모리를 관리하는 등 복잡한 작업이 필요하죠.
    • 프레임워크(Spring 등)는 이런 복잡한 작업들을 미리 다 구현해 놓고, 개발자가 짠 코드를 자기가 필요할 때 불러다가 써요. 그래서 개발자 입장에서는 "나의 코드 밖(외부)에서 무언가 내 코드를 통제하고 있다"라고 느껴지는 거예요.
  •  
  • 프레임워크 vs 라이브러리 (결정적 차이) 
    • 이 둘을 구분하는 가장 큰 기준이 바로 앞에서 배운 제어의 역전(IoC)입니다.
구분 주도권 (제어권) 비유 
라이브러리 개발자가 필요할 때 갖다 씀 내가 필요할 때 꺼내 쓰는 도구 (망치, 가위)
프레임워크 프레임워크가 개발자의 코드를 실행함 내가 들어가서 규칙대로 움직여야 하는 건물

 

  • 프레임워크의 핵심 요약
    • 재사용성: 자주 쓰이는 기능(보안, DB 연결 등)이 미리 만들어져 있음.
    • 강제성: 프레임워크가 정한 규칙을 따라야만 작동함. (이 규칙 덕분에 협업이 쉬워져요!)
    • 효율성: 뼈대를 만드는 수고를 덜고, 개발자는 핵심 기능 구현에만 집중할 수 있음.
결국 Spring이라는 프레임워크를 배운다는 건, 스프링이 미리 만들어 놓은 '거대한 집(뼈대)'에 들어가서 어떤 방에 어떤 가구(코드)를 배치해야 집이 잘 돌아가는지 그 규칙을 배우는 과정이라고 보시면 됩니다!

 

JPA

  • JPA (Java Persistence API)는 '규칙(명세서)'입니다.
    • JPA는 실제 코드가 들어가 있는 도구나 라이브러리가 아닙니다. 자바 진영에서 "앞으로 자바 객체랑 DB 테이블을 연결(ORM)할 때는 이런 규칙과 인터페이스를 쓰자!"라고 정해놓은 법안이자 설계도일 뿐입니다.
  • 실제 일은 '하이버네이트(Hibernate)'가 합니다. 설계도(JPA)가 있으면 실제 건물을 짓는 시공사가 있어야겠죠? JPA라는 규칙에 맞춰서 실제로 영속성 컨텍스트를 돌리고, 더티 체킹을 하고, 쿼리를 만들어 DB에 쏘는 '진짜 라이브러리'가 바로 하이버네이트입니다. 우리가 쓰는 스프링 부트 안에는 기본적으로 이 하이버네이트가 내장되어 있습니다.
💡 요약: ORM이라는 큰 범주 안에 자바의 표준JPA가 있고, 그 JPA를 실질적으로 구현한 것하이버네이트!

 

  • ORM: '스마트폰'이라는 개념
  • JPA: '전화가 되어야 하고, 앱을 깔 수 있어야 한다'는 설계 가이드라인
  • Hibernate: 갤럭시나 아이폰 같은 실제 제품

 

DB를 직접 다룰 때의 문제점

  1. SQL 직접 관리: 테이블 생성부터 INSERT/SELECT 쿼리 작성까지 개발자가 매번 직접 SQL을 관리해야 합니다.
  2. 반복적인 매핑 작업: DB에서 가져온 데이터(ResultSet)를 자바 객체(DTO)로 변환하는 코드를 일일이 짜야해서 손이 많이 갑니다.
  3. 변경에 취약함: 필드 하나(예: 비밀번호)만 추가해도 관련 SQL과 객체 변환 로직을 전부 찾아다니며 수정해야 합니다.
  4. 생산성 저하: 실제 서비스의 핵심 로직 개발보다, 단순 반복적인 SQL 수정과 매핑 작업에 더 많은 시간을 뺏기게 됩니다.
  5. 결론: "객체와 DB 사이의 'SQL 노가다'를 없애고, 자바 코드 중심으로 편하게 개발하기 위해 ORM(JPA)이 등장했다!"

 

ORM

  • ORM : Object-Relational Mapping
    • Object : "객체"지향 언어 (자바, 파이썬)
    • Relational : "관계형" 데이터베이스 (H2, MySQL)

📌 반복적이고 번거로운 애플리케이션 단에서의 SQL 작업을 줄여주기 위해서 ORM(객체 관계 매핑) 기술들이 등장하게 됩니다.
  • ORM은 이름 그대로 객체와 DB의 관계를 매핑해 주는 도구입니다.
    • 객체 즉, 자바의 클래스와 DB의 데이터를 직접 매핑하려면 매우 번거롭고 많은 작업들이 필요했지만 ORM을 사용하면 이를 자동으로 처리해 줍니다.
💡 요약 
  • JDBC: "DB에서 데이터를 가져오는 통로" 역할을 함.
  • 매핑(Mapping): "가져온 데이터를 자바 객체에 옮겨 담는 작업"인데, JDBC만 쓰면 이걸 사람이 일일이 다 해야 함.
  • 그래서?: 이 귀찮은 매핑 작업을 대신 자동으로 해주는 게 바로 JPA(ORM) 같은 기술들입니다!
    • "객체랑 DB 테이블이랑 서로 다르니까, 이걸 중간에서 자동으로 이어 주자!"라는 아이디어 그 자체예요.

 

JPA

  • JPA: Java Persistence API
  • 자바 ORM 기술에 대한 표준 명세

 

 

  • JPA는 Java ORM 기술의 대표적인 표준 명세입니다.
    • 자바에서 ORM을 구현하기 위해 정해놓은 표준 명세(명세서)입니다.
    • 실제로 코드가 동작하는 게 아니라, "자바 ORM이라면 이런 기능이 있어야 하고, 이런 메서드 이름을 써야 해"라고 정해둔 약속(Interface)입니다.

 

 

  • JPA는 애플리케이션과 JDBC 사이에서 동작되고 있습니다.
  • JPA를 사용하면 DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해 줍니다.
  • 또한 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있습니다.

 

하이버네이트(Hibernate)

  • JPA는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준하이버네이트입니다.
  • 스프링 부트에서는 기본적으로 ‘하이버네이트’ 구현체를 사용 중입니다.
사실상 표준 (de facto, 디팩토) 보통 기업 간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준이다
출처: 위키백과

 

Entity 이해하기

  • JPA에서 관리되는 클래스 즉, 객체를 의미합니다.
  • Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됩니다.

 

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 길이 지정
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}

 

  • @Entity : JPA가 관리할 수 있는 Entity 클래스로 지정할 수 있습니다.
    • @Entity(name = "Memo") : Entity 클래스 이름을 지정할 수 있습니다. (default: 클래스명)
    • JPA가 Entity 클래스를 인스턴스화할 때 기본 생성자를 사용하기 때문에 반드시 현재 Entity 클래스에서 기본 생성자가 생성되고 있는지 확인해야 합니다.
  • @Table : 매핑할 테이블을 지정해 줍니다.
    • @Table(name = "memo") : 매핑할 테이블의 이름을 지정할 수 있습니다. (default: Entity 명)
  • @Column :
    • @Column(name = "username") : 필드와 매핑할 테이블의 칼럼을 지정할 수 있습니다. (default: 객체의 필드명)
    • @Column(nullable = false) : 데이터의 null 값 허용 여부를 지정할 수 있습니다. (default: true)
    • @Column(unique = true) : 데이터의 중복 값 허용 여부를 지정할 수 있습니다. (default: false)
    • @Column(length = 500) : 데이터 값(문자)의 길이에 제약조건을 걸 수 있습니다. (default: 255)
  • @Id : 테이블의 기본 키를 지정해 줍니다.
    • 이 기본 키는 영속성 컨텍스트에서 Entity를 구분하고 관리할 때 사용되는 식별자 역할을 수행합니다.
    • 따라서 기본 키 즉, 식별자 값을 넣어주지 않고 저장하면 오류가 발생합니다.
    • @Id 옵션만 설정하면 기본 키 값을 개발자가 직접 확인하고 넣어줘야 하는 불편함이 발생합니다.
    • @GeneratedValue 옵션을 추가하면 기본 키 생성을 DB에 위임할 수 있습니다.
여러 가지 전략 중 IDENTITY 전략을 살펴보겠습니다.
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • id bigint not null auto_increment : auto_increment 조건이 추가된 것을 확인할 수 있습니다.
    • 해당 옵션을 추가해 주면 개발자가 직접 id 값을 넣어주지 않아도 자동으로 순서에 맞게 기본 키가 추가됩니다.

 

영속성 컨텍스트

  • Persistence를 한글로 번역하면 영속성, 지속성이라는 뜻이 됩니다.
  • Persistence를 객체의 관점으로 해석해 보자면 ‘객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할 수 있는 객체의 성질’을 의미합니다.
  • 영속성 콘텍스트를 좀 더 쉽게 표현해 보자면 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간입니다.

 

"애플리케이션과 DB 사이에서 엔티티를 효율적으로 관리(캐시, 변경 감지 등)하는 '논리적인 가상 저장소'
  • 메모리에서 처리: DB까지 매번 왔다 갔다 하면 느리니까, 일단 자기 메모리(1차 캐시)에 저장해 두고 거기서 꺼내 써요.
  • 변경 사항 추적: 객체 값이 바뀌었는지 자기 혼자 몰래 지켜보고 있다가(변경 감지), 나중에 한꺼번에 DB에 알려줘요.
  • 동기화: 작업이 다 끝나면(commit), 그때서야 이 가상공간에 있던 내용이 진짜 DB에 확! 반영되는 거죠.

 

  • 개발자들은 이제 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장하거나 조회할 수 있으며 수정, 삭제 또한 가능합니다.
  • 이러한 일련의 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트 Entity 객체들을 저장하여 관리하면서 DB와 소통합니다.

 

EntityManager

  • 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요합니다.
  • EntityManager는 이름 그대로 Entity를 관리하는 관리자입니다.
  • 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있습니다.
  • EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있습니다.

 

EntityManagerFactory

  • EntityManagerFactory는 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용됩니다.
  • EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야 합니다.
    • 정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="memo">
        <class>com.sparta.entity.Memo</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>

            <property name="hibernate.hbm2ddl.auto" value="create" />

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

 

EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();
  • EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
    • 해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성합니다.
  • EntityManager em = emf.createEntityManager();
    • 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있습니다.

 

JPA의 트랜잭션

트랜잭션

  • 트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념입니다.
    • 쉽게 표현하자면 DB의 데이터들을 안전하게 관리하기 위해서 생겨난 개념입니다.
  • 가장 큰 특징은 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있다는 점입니다.
  • 이때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌립니다.
START TRANSACTION; # 트랜잭션을 시작합니다.

INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;

COMMIT; # 트랜잭션을 커밋합니다.

SELECT * FROM memo;

 

  • JPA는 DB의 이러한 트랜잭션 개념을 사용하여 효율적으로 Entity를 관리하고 있습니다.

 

JPA의 트랜잭션

  • 영속성 컨텍스트에 Entity 객체들을 저장했다고 해서 DB에 바로 반영되지는 않습니다.

 

  • DB에서 하나의 트랜잭션에 여러 개의 SQL을 포함하고 있다가 마지막에 영구적으로 변경을 반영하는 것처럼
  • JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한 번에 DB에 요청해 변경을 반영합니다.

 

저장 성공 예제)
@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
    EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.

    et.begin(); // 트랜잭션을 시작합니다.

    try { // DB 작업을 수행합니다.

        Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
        memo.setId(1L); // 식별자 값을 넣어줍니다.
        memo.setUsername("Robbie");
        memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");

        em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.

        et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
        // commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
    } finally {
        em.close(); // 사용한 EntityManager 를 종료합니다.
    }

    emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}

 

  • JPA에서 이러한 트랜잭션의 개념을 적용하기 위해서는 EntityManager에서 EntityTransaction을 가져와 트랜잭션을 적용할 수 있습니다.
    • EntityTransaction et = em.getTransaction();
    • 해당 코드를 호출하여 EntityTransaction을 가져와 트랜잭션을 관리할 수 있습니다.
  • et.begin();
    • 트랜잭션을 시작하는 명령어입니다.
  • et.commit();
    • 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어입니다.
  • et.rollback();
    • 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어입니다.

 

저장 실패 예제)
@Test
@DisplayName("EntityTransaction 실패 테스트")
void test2() {
    EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.

    et.begin(); // 트랜잭션을 시작합니다.

    try { // DB 작업을 수행합니다.

        Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
        memo.setUsername("Robbert");
        memo.setContents("실패 케이스");

        em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.

        et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
        // commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
    } catch (Exception ex) {
        System.out.println("식별자 값을 넣어주지 않아 오류가 발생했습니다.");
        ex.printStackTrace();
        et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
    } finally {
        em.close(); // 사용한 EntityManager 를 종료합니다.
    }

    emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
  • 식별자 값을 넣어주지 않아 오류가 발생했습니다.
  • 따라서 et.rollback(); 코드가 호출이 되어 트랜잭션 작업 내용들이 취소되었습니다.
    • DB를 확인해 보면 해당 작업이 반영되어있지 않은 것을 확인할 수 있습니다.

 

영속성 컨텍스트 Debugging으로 확인하기

  • 1) 테스트 시작 위치 가장 왼쪽을 클릭하여 빨간색 원을 만듭니다. 그리고 해당 원에 우측클릭 후 Thread 부분을 클릭한 다음 Make Default 클릭 후 Done을 누릅니다.

  • 2) 초록색 화살표 시작 버튼을 클릭 후 Debug를 클릭합니다.

  • 3) 체크 부분을 클릭하여 순차적으로 코드를 진행합니다.

  • 4) em.persist(memo); 코드 호출 후 persistenceContext 위치에 memo 객체가 저장되어 있음을 확인할 수 있습니다.

 

영속성 컨텍스트의 기능

📌 영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간입니다.

  • 영속성 컨텍스트가 어떻게 Entity 객체를 효율적으로 관리하고 있는지 살펴보겠습니다.

 

1차 캐시

  • 영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있습니다.
    • 우리가 저장하는 Entity 객체들1차 캐시 즉, 캐시 저장소에 저장된다고 생각하시면 됩니다.
    • 캐시 저장소는 Map 자료구조 형태로 되어있습니다.
      1. key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장합니다.
      2. value에는 해당 Entity 클래스의 객체를 저장합니다.
      3. 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리합니다.
  • 영속성 컨텍스트가 이 캐시 저장소를 어떻게 활용하고 있는지 살펴보겠습니다.

 

Entity 저장

  • em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장합니다.
@Test
@DisplayName("1차 캐시 : Entity 저장")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("1차 캐시 Entity 저장");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em > persistenceContext > entitiesBykey를 확인해 보시면 key-value 형태로 정보가 저장되어 있음을 확인할 수 있습니다.

 

Entity 조회 

1) 캐시 저장소에 조회하는 Id가 존재하지 않은 경우

  • 캐시 저장소 조회

 

  • DB SELECT 조회 후 캐시 저장소에 저장

  • em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인한 후 해당 값이 없다면?
  • DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환합니다.

 

@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
void test2() {
    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());


    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}
  • DB에서 데이터를 조회만 하는 경우에는 데이터의 변경이 발생하는 것이 아니기 때문에 트랜잭션이 없어도 조회가 가능합니다.
  • Memo memo = em.find(Memo.class, 1); 호출 시 캐시 저장소에 해당 값이 존재하지 않기 때문에 DB에 SELECT 조회하여 캐시 저장소에 저장한 후 반환합니다.

 

2) 캐시 저장소에 조회하는 Id가 존재하는 경우

  • em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회합니다.
    • 값이 있다면 해당 Entity 객체를 반환합니다.

 

@Test
@DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하는 경우")
void test3() {
    try {

        Memo memo1 = em.find(Memo.class, 1);
        System.out.println("memo1 조회 후 캐시 저장소에 저장\n");

        Memo memo2 = em.find(Memo.class, 1);
        System.out.println("memo2.getId() = " + memo2.getId());
        System.out.println("memo2.getUsername() = " + memo2.getUsername());
        System.out.println("memo2.getContents() = " + memo2.getContents());


    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}
  • Memo memo1 = em.find(Memo.class, 1);
    • 호출 때는 캐시 저장소에 존재하지 않기 때문에 DB에 SELECT 조회하여 캐시 저장소에 저장합니다.
  • Memo memo2 = em.find(Memo.class, 1);
    • 를 호출했을 때는 이미 캐시 저장소에 해당 값이 존재하기 때문에 DB에 조회하지 않고 캐시 저장소에서 해당 값을 반환합니다.

 

'1차 캐시' 사용의 장점 

  1. DB 조회 횟수를 줄임
  2. '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)
  • 객체 동일성 보장
@Test
@DisplayName("객체 동일성 보장")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();
    
    try {
        Memo memo3 = new Memo();
        memo3.setId(2L);
        memo3.setUsername("Robbert");
        memo3.setContents("객체 동일성 보장");
        em.persist(memo3);

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 1);
        Memo memo  = em.find(Memo.class, 2);

        System.out.println(memo1 == memo2);
        System.out.println(memo1 == memo);

        et.commit();
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}
  • 같은 값을 조회하는 memo1과 memo2는 == 결과 true를 반환합니다.
  • memo1과 다른 값을 조회하는 memo는 == 결과 false를 반환합니다.

 

Entity 삭제

1) 삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에 조회해서 저장합니다.

 

2) em.remove(entity);

  • em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청됩니다.
@Test
@DisplayName("Entity 삭제")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 2);

        em.remove(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}
  • em.remove(memo); 호출되면서 memo Entity 객체를 DELETED 상태로 만들고 트랜잭션 commit 후 Delete SQL이 DB에 요청되었습니다.

  • em.find(Memo.class, 2); 호출하여 memo 객체를 캐시 저장소에 저장한 후
  • entityEntry를 확인해 보시면 memo Entity 객체가 영속성 컨텍스트가 관리하는 MANAGED 상태인 것을 확인할 수 있습니다.

  • em.remove(memo); 호출 후 memo Entity 객체가 DELETED 상태로 바뀐 것을 확인할 수 있습니다.
  • 트랜잭션 commit 후 DB 데이터를 확인해 보면 해당 데이터가 삭제되어 있는 것을 확인할 수 있습니다.

 

쓰기 지연 저장소(ActionQueue)

  • JPA의 트랜잭션을 학습하면서 JPA가 트랜잭션처럼 SQL을 모아서 한 번에 DB에 반영한다는 것을 배웠습니다.
    • JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한 번에 DB에 반영합니다.
  • Debugging을 통해 실제로 쓰기 지연 저장소에 SQL을 모아서 한 번에 반영하는지 확인해 보겠습니다.

 

@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(2L);
        memo.setUsername("Robbert");
        memo.setContents("쓰기 지연 저장소");
        em.persist(memo);

        Memo memo2 = new Memo();
        memo2.setId(3L);
        memo2.setUsername("Bob");
        memo2.setContents("과연 저장을 잘 하고 있을까?");
        em.persist(memo2);

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • [쓰기 지연 저장소 확인]: em > actionQueue를 확인해 보면 insertions > executables에 Insert 할 memo#2, memo#3 Entity 객체 2개가 들어가 있는 것을 확인할 수 있습니다.

  • [트랜잭션 commit 후]: actionQueue에 있던 insertions 데이터가 사라진 것을 확인할 수 있습니다.

  • 실제로 기록을 확인해 보면 트랜잭션 commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한 번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있습니다.

 

flush()

  • 트랜잭션 commit 후 쓰기 지연 저장소의 SQL들이 한 번에 요청됨을 확인했습니다.
  • 사실 트랜잭션 commit 후 추가적인 동작이 있는데 바로 em.flush(); 메서드의 호출입니다.
  • flush 메서드영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할을 수행합니다.
    • 즉, 쓰기 지연 저장소의 SQL들을 DB에 요청하는 역할을 수행합니다.

 

@Test
@DisplayName("flush() 메서드 확인")
void test7() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(4L);
        memo.setUsername("Flush");
        memo.setContents("Flush() 메서드 호출");
        em.persist(memo);

        System.out.println("flush() 전");
        em.flush(); // flush() 직접 호출
        System.out.println("flush() 후\n");
        

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.flush(); 메서드가 호출되자 바로 DB에 쓰기 지연 저장소의 SQL이 요청되었습니다.
  • 이미 쓰기 지연 저장소의 SQL이 요청되었기 때문에 더 이상 요청할 SQL이 없어 트랜잭션이 commit 된 후에 SQL 기록이 보이지 않습니다.

  • 추가) 트랜잭션을 설정하지 않고 플러시 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생합니다.
    • Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요합니다.

 

변경 감지(Dirty Checking)

  • 영속성 컨텍스트에 저장된 Entity가 변경될 때마다 Update SQL이 쓰기 지연 저장소에 저장된다면?
    • 하나의 Update SQL로 처리할 수 있는 상황을 여러 번 Update SQL을 요청하게 되기 때문에 비효율적입니다.
  • 그렇다면 JPA는 어떻게 Update를 처리할까요?
    • em.update(entity); 같은 메서드를 지원할 것 같지만 찾아볼 수 없습니다.

  • PA에서는 Update를 어떻게 처리하는지 살펴보겠습니다.
  • JPA영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장합니다.
    • 트랜잭션이 commit 되고 em.flush(); 가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교합니다.
    • 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기 지연 저장소의 SQL을 DB에 요청합니다.
    • 마지막으로 DB의 트랜잭션이 commit 되면서 반영됩니다.
  • 따라서 변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되고 DB에 반영됩니다.
    • 이러한 과정을 변경 감지, Dirty Checking이라 부릅니다.

 

@Test
@DisplayName("변경 감지 확인")
void test8() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        System.out.println("변경할 데이터를 조회합니다.");
        Memo memo = em.find(Memo.class, 4);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("\n수정을 진행합니다.");
        memo.setUsername("Update");
        memo.setContents("변경 감지 확인");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • entityInstance는 Entity 객체의 현재 상태입니다.
  • entityEntry > loadedState는 조회했을 때 즉, 해당 Entity의 최초 상태입니다.
  • 트랜잭션 commit 후 em.flush(); 메서드가 호출되면 현재 상태와 최초 상태를 비교하고 변경이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장한 후 DB에 요청합니다.

  • 수정을 진행하고 트랜잭션 commit 후 Update SQL이 요청된 것을 확인할 수 있습니다.

 

Entity의 상태

 

비영속(Transient)

Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
  • 쉽게 말하자면 new 연산자를 통해 인스턴스화된 Entity 객체를 의미합니다.
  • 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않습니다.

 

영속(Managed)

em.persist(memo);
  • persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듭니다.

 

@Test
@DisplayName("비영속과 영속 상태")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo(); // 비영속 상태
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("비영속과 영속 상태");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • 비영속 상태이기 때문에 entitiesByKey=null 입니다.
  • 비영속 상태는 JPA가 관리하지 못하기 때문에 해당 객체의 데이터를 변경해도 변경 감지가 이루어지지 않습니다.

  • em.persist(memo); 메서드 호출 후 영속성 컨텍스트에 저장되었고 MANAGED 상태 즉, JPA가 관리하는 영속 상태의 Entity가 되었습니다.

 

준영속(Detached)

  • 준영속 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미합니다.
영속 상태에서 준영속 상태로 바꾸는 방법

detach(entity)

em.detach(memo);
  • detach(entity) : 특정 Entity만 준영속 상태로 전환합니다.
    • 영속성 컨텍스트에서 관리되다(Managed)가 분리된 상태(Detached)로 전환됩니다.
@Test
@DisplayName("준영속 상태 : detach()")
void test2() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 1);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo);
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("memo Entity 객체 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.find(Memo.class, 1); 메서드 호출 후 MANAGED 상태입니다.

  • em.detach(memo); 메서드를 호출하여 특정 Entity 객체 Memo#1를 영속성 컨텍스트에서 제거하였습니다.
  • 준영속 상태로 전환되면 1차 캐시 즉, 캐시 저장소에서 제거되기 때문에 JPA의 관리를 받지 못해 영속성 컨텍스트의 어떠한 기능도 사용할 수 없습니다.

 

  • 따라서 memo Entity 객체의 데이터를 수정해도 변경감지 기능을 사용할 수 없어 Update SQL이 수행되지 않았습니다.
  • em.contains(memo); 는 해당 객체가 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드로 em.detach(memo); 이후 확인했을 때 false가 출력된 것을 확인할 수 있습니다.

 

clear()

em.clear();
  • clear() : 영속성 컨텍스트를 완전히 초기화합니다.
    • 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환합니다.
    • 영속성 컨텍스트 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태가 됩니다.
    • 따라서 계속해서 영속성 컨텍스트이용할 수 있습니다.
@Test
@DisplayName("준영속 상태 : clear()")
void test3() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("clear() 호출");
        em.clear();
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("memo#1 Entity 다시 조회");
        Memo memo = em.find(Memo.class, 1);
        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("\n memo Entity 수정 시도");
        memo.setUsername("Update");
        memo.setContents("memo Entity Update");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.clear(); 메서드 호출 후 완전히 비워진 영속성 컨텍스트를 확인할 수 있습니다.

  • 다시 memo#1 Entity를 조회하여 영속성 컨텍스트에 저장된 것을 확인할 수 있습니다.

  • em.clear(); 메서드 호출 후 em.contains(memo1,2); 확인했을 때 false가 출력된 것을 확인할 수 있습니다.
  • 다시 memo#1 Entity를 조회한 후 em.contains(memo); 확인했을 때 true가 출력된 것을 확인할 수 있습니다.
  • 또한 memo Entity 객체의 데이터를 수정하자 트랜잭션 commitUpdate SQL수행된 것을 확인할 수 있습니다.

 

close()

em.close();
  • close() : 영속성 컨텍스트를 종료합니다.
    • 해당 영속성 컨텍스트가 관리하던 영속성 상태의 Entity들은 모두 준영속 상태로 변경됩니다.
    • 영속성 컨텍스트가 종료되었기 때문에 계속해서 영속성 컨텍스트를 사용할 수 없습니다.
@Test
@DisplayName("준영속 상태 : close()")
void test4() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo1 = em.find(Memo.class, 1);
        Memo memo2 = em.find(Memo.class, 2);

        // em.contains(entity) : Entity 객체가 현재 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드
        System.out.println("em.contains(memo1) = " + em.contains(memo1));
        System.out.println("em.contains(memo2) = " + em.contains(memo2));

        System.out.println("close() 호출");
        em.close();
        Memo memo = em.find(Memo.class, 2); // Session/EntityManager is closed 메시지와 함께 오류 발생
        System.out.println("memo.getId() = " + memo.getId());

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.close(); 메서드 호출 이후 EntityManager를 사용하려고 하자 오류가 발생했습니다.
    • 영속성 컨텍스트가 종료되면 계속해서 영속성 컨텍스트를 사용할 수 없다는 것을 확인할 수 있습니다.

 

준영속 상태에서 다시 영속 상태로 바꾸는 방법

merge(entity)

em.merge(memo);
  • merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환합니다.
merge(entity) 동작
  • 파라미터로 전달된 Entity의 식별자 값으로 영속성 컨텍스트를 조회합니다.
    1. 해당 Entity가 영속성 컨텍스트에 없다면?
      • DB에서 새롭게 조회합니다.
      • 조회한 Entity를 영속성 컨텍스트에 저장합니다.
      • 전달받은 Entity의 값을 사용하여 병합합니다.
      • Update SQL이 수행됩니다. (수정)
    2. 만약 DB에서도 없다면?
      1. 새롭게 생성한 Entity를 영속성 컨텍스트에 저장합니다.
      2. Insert SQL이 수행됩니다. (저장)
  • 따라서 merge(entity) 메서드는 비영속, 준영속 모두 파라미터로 받을 수 있으며 상황에 따라 ‘저장’을 할 수도 ‘수정’을 할 수도 있습니다.

 

merge(entity) 저장
@Test
@DisplayName("merge() : 저장")
void test5() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo();
        memo.setId(3L);
        memo.setUsername("merge()");
        memo.setContents("merge() 저장");

        System.out.println("merge() 호출");
        Memo mergedMemo = em.merge(memo);

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

  • em.merge(memo); 호출 후 영속성 컨텍스트에 Memo#3 객체가 저장되고 Insert SQL이 추가된 것을 확인할 수 있습니다.

 

  • em.merge(memo); 호출 후 영속성 컨텍스트에 해당 값이 없어 DB에 조회 했는데도 해당 값이 없기 때문에 새롭게 생성하여 영속성 컨텍스트에 저장하고 Insert SQL이 수행되었습니다.
    • 비영속 상태의 memo는 merge() 호출 후에 해당 memo 객체가 영속성 컨텍스트에 저장된게 아니라 새롭게 생성되어 영속성 컨텍스트에 저장되었기 때문에 false가 반환되었습니다.
    • 새롭게 저장된 영속 상태의 객체를 반환받은 mergedMemotrue가 반환되었습니다.

 

merge(entity) 수정

 

@Test
@DisplayName("merge() : 수정")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = em.find(Memo.class, 3);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("detach() 호출");
        em.detach(memo); // 준영속 상태로 전환
        System.out.println("em.contains(memo) = " + em.contains(memo));

        System.out.println("준영속 memo 값 수정");
        memo.setContents("merge() 수정");

        System.out.println("\n merge() 호출");
        Memo mergedMemo = em.merge(memo);
        System.out.println("mergedMemo.getContents() = " + mergedMemo.getContents());

        System.out.println("em.contains(memo) = " + em.contains(memo));
        System.out.println("em.contains(mergedMemo) = " + em.contains(mergedMemo));

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

 

  • detach() 메서드 호출로 조회해 온 영속 상태의 memo 객체를 준영속 상태로 전환했습니다.
  • 준영속 상태의 memo의 값을 수정한 후 memo 객체를 사용해서 merge() 메서드를 호출했습니다.
  •  memo 객체는 준영속 상태이기 때문에 현재 영속성 컨텍스트에는 해당 객체가 존재하지 않습니다.
    • 따라서 DB에서 식별자 값을 사용하여 조회한 후 영속성 컨텍스트에 저장하고 파라미터로 받아온 준영속 상태의 memo 객체의 값을 새롭게 저장한 영속 상태의 객체에 병합하고 반환합니다.
    • 그 결과 반환된 mergedMemo의 contents를 출력하였을 때 변경되었던 내용인 “merge() 수정”이 출력되었습니다.
    • 트랜잭션 commitUpdate SQL이 수행됩니다.
  • 준영속 상태의 Entity memo는 merge() 호출 후에도 영속성 컨텍스트에 저장되어 있지 않기 때문에 false가 반환되었고
  • 새롭게 저장된 영속 상태의 객체를 반환받은 mergedMemotrue가 반환되었습니다.

 

삭제(Removed)

em.remove(memo);
  • remove(entity) : 삭제하기 위해 조회해 온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환합니다.

 

SpringBoot의 JPA

application.properties : Hibernate 설정
  • show_sql, format_sql, use_sql_comments 옵션
    • Hibernate가 DB에 요청하는 모든 SQL을 보기 좋게 출력해줍니다.
  • ddl-auto
    • create : 기존 테이블 삭제 후 다시 생성합니다. (DROP + CREATE)
    • create-drop : create와 같으나 종료시점에 테이블을 DROP 합니다.
    • update : 변경된 부분만 반영합니다.
    • validate : Entity와 테이블이 정상 매핑되었는지만 확인합니다.
    • none: 아무것도 하지 않습니다.

 

  • SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해 줍니다.
    • application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성됩니다.
@PersistenceContext
EntityManager em;
  • @PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager주입받아 사용할 수 있습니다.

 

Spring의 트랜잭션

  • Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공합니다.
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
						...
			
		@Transactional
		@Override
		public <S extends T> S save(S entity) {
		
			Assert.notNull(entity, "Entity must not be null");
		
			if (entityInformation.isNew(entity)) {
				em.persist(entity);
				return entity;
			} else {
				return em.merge(entity);
			}
		}

						...
}
  • 예시 코드처럼 @Transactional 애너테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있습니다.
    • 메서드가 호출되면, 해당 메서드 내에서 수행되는 모든 DB 연산 내용하나의 트랜잭션으로 묶입니다.
    • 이때, 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백합니다.
    • 클래스에 선언한 @Transactional해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여합니다.
    • 이때, save 메서드는 @Transactional 애너테이션이 추가되어 있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 적용됩니다.
readOnly = true 옵션
  • 트랜잭션에서 데이터를 읽기만 할 때 사용됩니다.
  • 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행할 수 있습니다.
  • 만약, 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생하기 때문에 주의해야 합니다.

 

@Transactional

트랜잭션 테스트
@Test
@Transactional 
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
  • 트랜잭션이 적용되어 DB 작업이 성공했습니다.
@Test
@DisplayName("메모 생성 실패")
void test2() {
    Memo memo = new Memo();
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
  • 트랜잭션이 적용되지 못해 작업이 취소되었습니다.
  • 즉, JPA를 사용하여 DB에 데이터를 저장, 수정, 삭제하려면 트랜잭션 적용이 반드시 필요합니다.
    • 조회 작업은 단순하게 데이터를 읽기만 하기 때문에 트랜잭션 적용이 필수는 아닙니다.
    • 다만 조회의 경우에도 트랜잭션 환경이 필요한 경우가 있을 수 있기 때문에 조회 작업 기능만 존재하는 메서드일 경우에만 앞서 본 예시처럼 readOnly = true 옵션이 설정된 @Transactional을 적용하면 좋습니다.

 

영속성 컨텍스트와 트랜잭션의 생명주기

  • 스프링 컨테이너 환경에서는 영속성 컨텍스트트랜잭션의 생명주기일치합니다.
  • 쉽게 설명하자면 트랜잭션이 유지되는 동안은 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능을 사용할 수 있습니다.
  • 따라서 앞에서 작성한 테스트 코드 메서드에 트랜잭션이 적용되지 않았기 때문에 영속성 컨텍스트가 유지되지 못해 오류가 발생했었습니다.

 

⚠️ Spring은 어떻게 Service부터 Repository까지 Transaction을 유지할 수 있는 걸까요?
  • Service의 트랜잭션이 적용된 메서드에서 Repository의 메서드를 호출할 때 무언가 처리되고 있는 것이 있는 걸까요?
  • Spring에서는 이러한 상황에서 트랜잭션을 제어할 수 있도록 트랜잭션 전파 기능을 제공하고 있습니다. 

 

트랜잭션 전파 

  • @Transactional에서 트랜잭션 전파 옵션을 지정할 수 있습니다.

  • 트랜잭션 전파의 기본 옵션REQUIRED 입니다.

  • REQUIRED 옵션은 부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류하게 됩니다.

 

트랜잭션 전파 테스트 
@Transactional
public Memo createMemo(EntityManager em) {
    Memo memo = em.find(Memo.class, 1);
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 전파 테스트 중!");

    System.out.println("createMemo 메서드 종료");
    return memo;
}
package com.sparta.memo;

import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
public class TransactionTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    MemoRepository memoRepository;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Disabled
    @DisplayName("메모 생성 실패")
    void test2() {
        Memo memo = new Memo();
        memo.setUsername("Robbie");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }
}

 

  • 실행 결과 자식 메서드 createMemo가 종료될 때 update가 실행되는 것이 아니라 부모 메서드에 트랜잭션이 합류되면서 부모 메서드가 종료된 후 트랜잭션이 커밋될 때 update가 실행된 것을 확인할 수 있습니다.

 

  • 부모 메서드 test3의 @Transactional @Rollback(value = false)을 주석 처리하고 다시 한번 update를 시도해 보면 합류할 부모 트랜잭션이 없기 때문에

  • 이처럼 자식 메서드가 종료된 후 트랜잭션이 커밋되면서 update가 실행된 것을 확인할 수 있습니다.

 

Spring Data JPA

  • Spring Data JPAJPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈입니다.
    • JPA를 추상화시킨 Repository 인터페이스를 제공합니다.
  • Repository 인터페이스Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용됩니다.
    • 개발자들은 Repository 인터페이스를 통해 JPA를 간편하게 사용할 수 있게 되었습니다.

 

JPA라는 '표준'을 더 쓰기 편하게 '도구화'한 것이 Spring Data JPA의 Repository

 

  • JPA (표준 명세): "DB랑 대화할 땐 EntityManager라는 반장을 통해서 em.persist(), em.find() 같은 명령어를 써라!"라고 정해둔 규칙이에요.
  • Spring Data JPA (편의 도구): "매번 EntityManager 불러오고 트랜잭션 걸고 하는 거 귀찮지? 그냥 인터페이스만 선언해. 구현체(클래스)는 내가 실행 시점에 자동으로 만들어서 꽂아줄게!"

 

 

  • Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성해 줍니다.
    • Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면,
    • 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring ‘Bean’으로 등록합니다.
  • 따라서 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있습니다.

 

Spring Data JPA 사용방법

  • JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface로 선언합니다.
    • Spring Data JPA에 의해 자동으로 Bean 등록이 되었습니다.
    • 제네릭스의 @Entity 클래스 위치에 Memo Entity를 추가했기 때문에 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 되었습니다.

 

save

public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    // RequestDto -> Entity
    Memo memo = new Memo(requestDto);

    // DB 저장
    Memo saveMemo = memoRepository.save(memo);

    // Entity -> ResponseDto
    MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);

    return memoResponseDto;
}

  • SimpleJpaRepository의 save 메서드를 확인해 보면 영속성 컨텍스트에 entity를 저장하는 코드가 작성되어 있습니다.
  • save 메서드를 사용해 데이터를 저장할 수 있습니다.
    • 파라미터로는 저장하려는 entity 객체를 넣어주면 됩니다.
    • 해당 메서드에 @Transactional이 적용되어 있는 것을 확인할 수 있습니다.

 

findAll

public List<MemoResponseDto> getMemos() {
    // DB 조회
    return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}

  • findAll 메서드를 사용해 해당 테이블의 전체 데이터를 조회할 수 있습니다.

 

findById

private Memo findMemo(Long id) {
    return memoRepository.findById(id).orElseThrow(() ->
            new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
    );
}

  • SimpleJpaRepository의 findById 메서드를 확인해 보면 반환 타입이 Optional인 것을 확인할 수 있습니다.
    • 파라미터로는 삭제하고자 하는 Entity의 id 값을 넣어주면 됩니다.
  • Optional<Entity 타입>을 반환 타입으로 받고 추가적으로 null을 체크하거나
  • 위 코드와 같이 orElseThrow를 사용하여 반환 값이 null일 경우 예외를 던지도록 처리할 수 있습니다.
  • 최대한 코드 수를 적게 하기 위해 orElseThrow를 사용하겠습니다.

https://www.baeldung.com/java-optional

 

Guide To Java Optional | Baeldung

Quick and practical guide to the Java Optional class

www.baeldung.com

 

update

@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    // 해당 메모가 DB에 존재하는지 확인
    Memo memo = findMemo(id);

    // memo 내용 수정
    memo.update(requestDto);

    return id;
}
  • SimpleJpaRepository에 update라는 메서드는 존재하지 않습니다.
  • 따라서 위 코드처럼 영속성 컨텍스트의 변경감지를 통해 update를 진행하겠습니다.
  • 변경감지가 적용되기 위해 해당 메서드에 @Transactional을 추가하였습니다.

 

delete

public Long deleteMemo(Long id) {
    // 해당 메모가 DB에 존재하는지 확인
    Memo memo = findMemo(id);

    // memo 삭제
    memoRepository.delete(memo);

    return id;
}

  • delete 메서드를 사용해 해당 Entity(데이터)를 테이블에서 삭제할 수 있습니다.
  • 파라미터로는 삭제하려는 entity 객체를 넣어주면 됩니다.
  • delete 메서드에 @Transactional이 적용되어 있는 것을 확인할 수 있습니다.

 

JPA Auditing 적용하기

Timestamped

📌 데이터의 생성(created_at), 수정(modified_at) 시간은 포스팅, 게시글, 댓글 등 다양한 데이터에 매우 자주 활용됩니다. 각각의 Entity의 생성 수정 시간을 매번 작성하는 건 너무 비효율적입니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}
  • Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능JPA Auditing을 제공하고 있습니다.
  • @MappedSuperclass
    • JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있습니다.
    • 즉, 객체의 상속 구조를 활용해 '공통 컬럼(생성/수정일 등)의 매핑 정보'만 자식 엔티티에게 물려주는 매핑 전용 부모 클래스 선언입니다.
  • @EntityListeners(AuditingEntityListener.class)
    • 해당 클래스에 Auditing 기능을 포함시켜 줍니다.
  • @CreatedDate
    • Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장됩니다.
    • 최초 생성 시간이 저장되고 그 이후에는 수정되면 안 되기 때문에 updatable = false 옵션을 추가합니다.
  • @LastModifiedDate
    • 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장됩니다.
    • 처음 생성 시간이 저장된 이후 변경이 일어날 때마다 해당 변경시간으로 업데이트됩니다.
  • @Temporal
    • 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용합니다.
    • DB에는 Date(날짜), Time(시간), Timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재합니다.
      1. DATE : ex) 2023-01-01
      2. TIME : ex) 20:21:14
      3. TIMESTAMP : ex) 2023-01-01 20:22:38.771000

 

⚠️ @SpringBootApplication이 있는 class에 @EnableJpaAuditing 추가!
  • JPA Auditing 기능을 사용하겠다는 정보를 전달해 주기 위해 @EnableJpaAuditing 을 추가해야 합니다.

 

Query Methods

  • Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공합니다.

  • JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어 있는 테이블에 요청하고자 하는 SQL을 메서드 이름을 사용하여 선언할 수 있습니다.
  • SimpleJpaRepository 클래스가 생성될 때 위처럼 직접 선언한 JpaRepository 인터페이스의 모든 메서드를 자동으로 구현해 줍니다.
    • JpaRepository 인터페이스의 메서드 즉, Query Methods는 개발자가 이미 정의되어있는 규칙에 맞게 메서드를 선언하면 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현이 됩니다.
    • 따라서 우리는 인터페이스에 필요한 SQL에 해당하는 메서드 이름 패턴으로 메서드를 선언하기만 하면 따로 구현하지 않아도 사용할 수 있습니다.
  • findAllByOrderByModifiedAtDesc 해당 메서드 이름은 Memo 테이블에서 ModifiedAt 즉, 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하는 메서드를 생성할 수 있습니다.
  • List<Memo> findAllByUsername(String username);
    • 이렇게 Query Method를 선언했을 경우 ByUsername에 값을 전달해줘야 하기 때문에 파라미터에 해당 값의 타입과 변수명을 선언해 줍니다.
    • 즉, Query Methods메서드의 파라미터를 통해 SQL에 필요한 값을 동적으로 받아 처리할 수 있습니다.

 

'Back-End > Spring' 카테고리의 다른 글

프로젝트 관리 심화: 챕터 2 (CI/CD)  (0) 2026.05.14
프로젝트 관리 심화: 챕터 1 (Docker)  (0) 2026.05.04
MSA (Microservice Architecture)  (0) 2026.04.14
Spring 숙련: 챕터 2  (0) 2026.04.08
Spring 숙련: 챕터 1  (0) 2026.04.07