📢 오늘의 목표 📢
✔️ 알고리즘, SQL 문제 풀이
✔️ 알고리즘 코드카타
✔️ SQL 코드카타
✔️ 프로그래머스 Level 2
✔️ 스프링 입문 강의
✔️ 1주 차
✔️ 2주 차 (~2-10)
⏱️ 오늘의 일정 ⏱️
9:00 ~ 10:00 - 알고리즘 코드카타
10:00 ~ 10:30 - 스프링 입문 주차 발제
10:30 ~ 11:30 - 새로운 팀과 인사 및 노션 작성
11:30 ~ 13:00 - 스프링 입문 강의
13:00 ~ 14:00 - 점심시간
14:00 ~ 18:00 - 스프링 입문 강의
18:00 ~ 19:00 - 저녁 시간
19:00 ~ 20:00 - 스프링 입문 강의
20:00 ~ 21:00 - TIL 작성
📜 Chapter 1. 알고리즘 문제 풀기
✔️ 알고리즘 코드카타
두 정수 사이의 합
✔️ SQL 코드카타
✔️ 프로그래머스 Level 2
📜 Chapter 2. 주말에 한 스프링 공부
✔️ DI 의존성 주입
의존성이란? 변수에 값을 할당하는 순간 생기는 것.
DI는 외부에 있는 의존 대상을 주입하는 것을 말한다.
✔️ 스프링 없이 의존성 주입하기
1 생성자를 통한 의존성 주입
Car 클래스는 Tire를 구현한 클래스들에 대해서 알 필요가 없다. (전략 패턴의 응용)
2 속성을 통한 의존성 주입
생성자는 객체 생성 시에만 의존성을 주입할 수 있으나 setter를 이용하면 언제든 주입 가능.
✔️ 스프링을 통한 의존성 주입
1. XML 파일 사용
<bean id="tire" class="expert002.KoreaTire"></bean>
<bean id="americaTire" class="expert002.AmericaTire"></bean>
<bean id="car" class="expert002.Car"></bean>
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("expert002.xml");
Car car = context.getBean("car", Car.class);
Tire tire = context.getBean("tire", Tire.class);
car.setTire(tire);
System.out.println(car.getTireBrand());
}
}
유니티를 사용하면서 이미 DI 라이브러리를 쓴 적이 있어서 다행이다.
등록하고 가져오는 과정이 유사하다.
책에서는 expert002 패키지에 xml 파일을 만들라고만 했는데 부트라서 좀 다른가 리소스 폴더에서 생성해야만 했다.
안 그러면 파일을 찾을 수 없다는 오류가 뜬다.
여기서 다른 타이어로 바꾸고 싶어도 xml 파일에서 id 매핑만 수정하면 되기 때문에 재배포할 필요가 없다!
아마 이 책에서는 스프링 MVC로 학습하고 있어 스프링 부트와는 차이가 좀 있을 것이다.
2. 스프링 설정 파일(XML)에서 속성 주입
<bean id="koreaTire" class="expert003.KoreaTire"></bean>
<bean id="americaTire" class="expert003.AmericaTire"></bean>
<bean id="car" class="expert003.Car">
<property name="tire" ref="koreaTire"></property>
</bean>
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("expert003.xml");
Car car = context.getBean("car", Car.class);
System.out.println(car.getTireBrand());
}
}
속성을 주입하는 부분까지 XML 파일에서 해주고 있다.
koreaTire를 Car클래스의 속성 중 tire라는 이름을 가진 놈한테 넣어주고 있다.
그래서 setter를 이용해 넣어 줄 필요도, 사용할 tire를 컨테이너에서 가져 올 필요도 없어졌으니 코드 두 줄이 사라진 것이다.
3. @Autowired를 통한 속성 주입
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<context:annotation-config/>
<bean id="tire" class="expert004.KoreaTire"></bean>
<bean id="americaTire" class="expert004.AmericaTire"></bean>
<bean id="car" class="expert004.Car"></bean>
</beans>
import org.springframework.beans.factory.annotation.Autowired;
public class Car {
@Autowired
Tire tire;
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
setter를 만들지 않고 속성을 주입하는 방법.
context 네임스페이스를 추가하며 xml 파일에서 속성을 주입하던 부분이 사라졌다.
그리고 원래 getter, setter를 사용하던 속성에 @Autowired라는 애너테이션이 붙었다.
아 이거 저번 스프링 책에서도 자주 봐서 익숙한 거다. 역시 공부를 하는 보람이 있다.
id가 tire인 클래스를 자동으로 생성해서 속성에 넣어주고 있다.
그런데
<context:annotation-config/>
<bean class="expert004.KoreaTire"></bean>
<bean id="car" class="expert004.Car"></bean>
여기서 타이어 하나만 남기고, id="tire" 부분을 없애도 정상적으로 작동한다.
바로 @Autowired의 기능으로 타입을 보고 매칭 해준 것.
타입이 여러 개일 경우에만 id로 매칭한다.
type이 id보다 우선순위가 높기 때문에 타입이 하나 뿐이라면 id는 무엇이 되어도 상관없다(물론 작명은 항상 신경 쓰자).
4. @Resource를 통한 속성 주입
public class Car {
@Resource
Tire tire;
public String getTireBrand() {
return "장착된 타이어: " + tire.getBrand();
}
}
@Autowired 애너테이션을 @Resource로 변경했을 뿐이다.
그러나 @Resource는 자바의 표준 애너테이션, @Autowired는 스프링 프레임워크의 애너테이션이라는 차이가 있다.
또 Resource는 Autowired와 반대로 id가 type보다 우선순위가 높다.
@Resource(name = "이름")
@Autowired
@Qualifier("이름")
타입이 여러개 있고 id를 쓰지 않더라도 위와 같은 방식으로 매칭할 수도 있다.
📜 Chapter 3. 스프링 입문 강의
✔️ 1-3 그래이들(Gradle)이란 무엇일까?
빌드 자동화 시스템: 작성한 소스코드를 실행 가능한 jar 파일로 만들어준다.
build.gradke
그래이들 기반의 빌드 스크립트. 라이브러리 간의 의존성도 관리해 준다.
라이브러리를 dependencies라는 부분에 작성하면 외부 저장소에서 가져온다.
plugins에서 버전을 맞춰줄 수 있다.
✔️ 1-4 서버란 무엇일까?
웹 서버 동작
- 브라우저를 통해 HTTP Request로 웹사이트를 웹서버에 요청.
- 이후 웹서버는 요청을 승인하고 HTTP Response를 통해 웹사이트 데이터를 브라우저에 전송.
- 마지막으로 브라우저는 서버에서 받아온 데이터를 이용해 웹사이트를 브라우저에 그린다.
RESTful API (Representational State Transfer(REST))
API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처.
REST 아키텍처 스타일을 따르는 API를 REST API라고 한다.
REST 아키텍처를 구현하는 웹 서비스를 RESTful 웹 서비스라고 한다.
Apache, Tomcat
웹 서버는 HTML 문서와 같은 정적인 콘텐츠만 전달할 수 있다.
마이페이지 같은 동적인 요청이 들어왔을 때는 WAS에 전달한다.
- Web Application Server(WAS)의 종류로는 Apache, Nginx 등이 있다.
WAS를 사용하면 로그인, 회원가입을 처리하거나 게시물을 조회하거나 정렬하는 등의 다양한 로직들을 수행하는 프로그램을 동작시킬 수 있다.
- 이 프로그램의 종류로는 Tomcat, JBoss 등이 있습니다.
스프링 부트
기존 스프링 프레임워크의 개선으로 xml 설정 대신 Java의 애너테이션 기반의 설정을 적극적으로 사용한다.
default 설정 값을 넣어주고, 외부 라이브러리와 하위 프레임워크의 의존성 관리가 편해졌다.
Apache Tomcat이 내장되어 있다(starter-web dependency를 설정 시).
Postman
API 개발을 빠르고 쉽게 구현할 수 있는 소프트웨어 플랫폼
✔️ 1-5 HTTP란 무엇일까?
데이터를 주고받는 양식을 정의한 "통신 규약"중 하나.
HTTP에서는 언제나 Request, Response라는 개념이 존재한다.
HTTP의 구성 요소
- Method (호출/요청 방식)
- Header (추가 데이터. 메타 데이터): 의사 표현을 위한 데이터
- Payload (데이터. 실제 데이터): 클라이언트(브라우저)가 요청을 할 때에도 Payload를 보낼 수 있다(GET 제외).
✔️ 1-6 테스트 코드
JUnit: 자바 프로그래밍 언어 용 단위 테스트 프레임워크.
클래스 우클릭 -> Generate -> Test -> OK
테스트 코드
package com.sparta.springprepare.calculator;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class CalculatorTest {
@Test
@DisplayName("더하기 테스트")
void test1() {
Calculator calculator = new Calculator();
Double result = calculator.operate(8, "+", 2);
System.out.println("result = " + result);
Assertions.assertEquals(10, result);
}
@Test
@DisplayName("나누기 테스트")
void test2() {
Calculator calculator = new Calculator();
Double result = calculator.operate(8, "/", 2);
System.out.println("result = " + result);
Assertions.assertEquals(4, result);
}
}
Assertions.assertEquals(10, result);
위 코드를 통해서 결괏값이 10과 같은지 테스트한다.
✔️ 1-7 Lombok과 application.properties
Annotation Processors에서 Enable annotation processing 체크.
프로젝트 생성할 때 롬복을 이미 설치했음.
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Memo {
private String username;
private String contents;
}
애너테이션 작성 시 자동으로 Getter, Setter생성.
빌드파일을 보면 생성되어 있는 걸 볼 수 있다.
@AllArgsConstructor
@NoArgsConstructor
위 애너테이션들은 생성자를 만들어주는 애너테이션이다.
@AllArgsConstructor는 모든 필드를 파라미터로 가지는 생성자를 만들어준다.
@NoArgsConstructor는 기본 생성자를 만들어준다.
@RequiredArgsConstructor
public class Memo {
private final String username;
private String contents;
}
@RequiredArgsConstructor는 final이 붙은 필드만 파라미터로 가지는 생성자를 만들어준다.
application.properties는 Spring과 관련된 설정을 할 때 사용되는 파일이다.
자동으로 설정되고 있는 설정 값을 쉽게 수정할 수 있다.
DB 연결 시 DB의 정보를 이 파일을 이용하여 전달할 수 있다.
✔️ 1-9 Spring MVC란 무엇일까?
이 강의는 나중에 다시 한번 들어봐야겠다.
MVC 패턴은 소프트웨어를 구성하는 요소들을 Model, View, Controller로 구분하여 각각의 역할을 분리한다.
Model: 데이터와 비즈니스 로직을 담당하고 데이터베이스와 연동.
View: 사용자 인터페이스를 담당한다.
Controller: Model과 View 사이의 상호작용을 조정하고 제어. 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트.
Spring Web MVC는 Servlet API를 기반으로 구축된 독창적인 웹 프레임워크이다.
Servlet (서블릿)은 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램 혹은 그 사양이다.
Front Controller 패턴
- DispatcherServlet는 Http 요청이 들어오면 요청을 분석한다.
- 우리가 작성한 코드에서 해당되는 controller를 찾아 요청을 전달해 준다.
- controller가 데이터('Model')와 'View' 정보를 다시 DispatcherServlet에 전달한다.
- ViewResolver 통해 View에 Model을 적용하여 View를 Client에게 응답으로 전달한다.
원래는 Servlet을 구현해줘야 했는데 이제 DispatcherServlet이 알아서 다 해준다는 것이다.
Servlet에 대한 설명은 아직 잘 모르겠는데 일단 강의를 다 듣고 이런 개념적인 부분을 집고 가야겠다.
✔️ 1-10 Controller 이해하기
경로는 중복이 될 수 있으나 메서드 중복은 불가능하다.
@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 요청";
}
}
경로 중 api는 중복이 되니 RequestMapping 애너테이션으로 공통 경로를 만들어줬다.
✔️ 1-11 정적 페이지와 동적 페이지
resources의 static 폴더에 있는 정적 페이지는 바로 파일 이름으로 접근할 수 있다.
타임리프 기본 설정 때문에 기본 경로가 templete 폴더로 매핑되어 있어
@GetMapping("/stati-hello")
public String hello() {
return "hello.html";
}
현재는 이 방법으로 페이지를 가져올 수 없다.
@GetMapping("/html/redirect")
public String redirect() {
return "redirect:/hello.html";
}
리다이렉트를 사용하면 컨트롤러를 타고 접근할 수 있다.
@GetMapping("/html/templates")
public String htmlTemplates() {
return "hello";
}
정적 웹페이지도 templates에 넣어도 된다. templates에 넣으면. html을 붙이지 않아도 된다.
동적 페이지
private static int visitCount = 0;
@GetMapping("/html/dynamic")
public String htmlDynamic(Model model) {
visitCount++;
model.addAttribute("visits", visitCount);
return "hello-visit";
}
방문자 수를 출력해 주는 페이지를 만들자.
import org.springframework.ui.Model;
model은 위 패키지를 import 해줘야 한다.
model을 통해 controller에서 템플릿(view)에 데이터를 전달한다. visitCount라는 변수를 visits라는 이름으로 전달하였다.
✔️ 1-12 데이터를 Client에 반환하는 방법
최근엔 html 파일을 그대로 반환하기보다는 json형태를 이용하는 편이다.
@Controller
@RequestMapping("/response")
public class ResponseController {
@GetMapping("/json/string")
@ResponseBody
public String helloStringJson() {
return "{\"name\":\"Robbie\",\"age\":95}";
}
@GetMapping("/json/class")
@ResponseBody
public Star helloClassJson() {
return new Star("Robbie", 95);
}
}
자바에는 json 타입이 따로 없기 때문에 위처럼 String으로 json을 반환하면 http가 text/html로 인식하고,
클래스를 만들어서 반환하면(스프링 내부에서 json으로 만들어준다) application/json으로 인식한다.
@RestController
@RequestMapping("/response/rest")
public class ResponseRestController {
// [Response header]
// Content-Type: text/html
// [Response body]
// {"name":"Robbie","age":95}
@GetMapping("/json/string")
public String helloStringJson() {
return "{\"name\":\"Robbie\",\"age\":95}";
}
// [Response header]
// Content-Type: application/json
// [Response body]
// {"name":"Robbie","age":95}
@GetMapping("/json/class")
public Star helloClassJson() {
return new Star("Robbie", 95);
}
}
@RestController를 달아주면 @ResponseBody를 넣을 필요가 없어진다.
참고로 ResponseBody는 템플릿을 반환하는 것이 아니라 반환 값 그대로 클라이언트에 반환하겠다는 말.
실제로 백엔드 개발자는 거의 json을 반환하는 일만 있다고 한다.
✔️ 1-13 Jackson이란 무엇일까?
Jackson은 JSON 데이터 구조를 처리해 주는 라이브러리로 Object와 JSON타입의 String을 변환할 수 있다.
@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);
}
@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());
}
직렬화에 getter를 사용하기 때문에 클래스에 꼭 getter를 설정해주어야 한다.
역직렬화에는 getter와 더불어 기본 생성자도 필요하다. getter 대신 setter를 사용할 수도 있다.
✔️ 1-14 Path Variable과 Request Param
브라우저에서 서버로 HTTP 요청을 보낼 때 데이터를 함께 보낼 수 있다.
@Controller
@RequestMapping("/hello/request")
public class RequestController {
@GetMapping("/form/html")
public String helloForm() {
return "hello-request-form";
}
// [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);
}
// [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);
}
// [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);
}
}
Path Variable 방식
데이터를 URL 경로에 추가.
URL 경로에서 데이터를 받고자 하는 위치의 경로에 {data} 중괄호를 사용
메서드 파라미터에 @PathVariable 애너테이션과 함께 {name} 중괄호에 선언한 변수명과 변수 타입을 선언하면 해당 경로의 데이터를 받아올 수 있다.
Request Param 방식
데이터를 URL 경로 마지막에? 와 & 를 사용하여 추가.
? name=Robbie&age=95에서 key 부분에 선언한 name과 age를 사용하여 value에 선언된 Robbie, 95 데이터를 받아올 수 있다.
메서드 파라미터에 @RequestParam 애너테이션과 함께 key 부분에 선언한 변수명과 변수 타입을 선언하면 데이터를 받아올 수 있다.
form 태그 POST
HTML의 form 태그를 사용하여 POST 방식으로 HTTP 요청을 보낼 수 있다.
HTTP Body에 name=Robbie&age=95 형태로 담겨 서버로 전달된다.
@RequestParam은 생략이 가능하다.
@RequestParam(required = false)로 설정하면 클라이언트에서 해당 값이 들어오지 않아도 오류가 발생하지 않는다.
@PathVariable도 동일한 옵션이 존대한다.
값이 들어오지 않으면 null로 초기화된다.
✔️ 1-15 HTTP 데이터를 객체로 처리하는 방법
// [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);
}
// [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);
}
@ModelAttribute 애너테이션으로 쿼리 String 형식을 객체로 바꾸어준다.
Body로 들어올 때는 물론이고 쿼리로 들어올 때도 객체로 바꿔 받을 수 있다.
그리고 이 @ModelAttribute는 생략이 가능하다.
스프링은 이 파라미터가 심플 데이터 타입인지 아닌지를 파악하여 @RequestParam인지 @ModelAttribute인지 구분한다.
// [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);
}
Body에 Json으로 넘어왔을 때는 @RequestBody 애너테이션으로 받아줘야 한다.
✔️ 1-16 메모장 프로젝트 설계
✔️ 1-17 Create, Read 구현하기
DTO(Data Transfer Object)는 데이터 전송 및 이동을 위해 생성되는 객체이다.
- Client에서 보내오는 데이터를 객체로 처리할 때 사용
- 서버의 계층간의 이동에 사용
- DB와의 소통을 담당하는 Java 클래스를 DTO로 한번 변환한 후 Client에 반환
@Getter
@Setter
@NoArgsConstructor
public class Memo {
private Long id;
private String username;
private String contents;
public Memo(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
}
먼저 DB에 저장할 형식을 갖춘 메모 엔티티를 만든다.
@Getter
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.username = memo.getUsername();
this.contents = memo.getContents();
}
}
@Getter
public class MemoRequestDto {
private String username;
private String contents;
}
메모를 받을 때, 반환할 때를 구분하여 리스폰스, 리퀘스트 Dto 또한 만들어 준다.
private final Map<Long, Memo> memoList = new HashMap<>();
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// Memo Max ID Check
Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
memo.setId(maxId);
// DB 저장
memoList.put(memo.getId(), memo);
// Entity -> ResponsDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
// Map To List
List<MemoResponseDto> responseList = memoList.values().stream()
.map(MemoResponseDto::new).toList();
return responseList;
}
컨트롤러에서 일단 crud의 c, r에 해당하는 메서드를 만들어 봤다.
일단 DB를 배우지 않았기 때문에 대신 HashMap을 사용하였다.
create에서는 requestdto 객체로 받아서 엔티티로 변환 후 DB에 저장, 다시 responsdto로 변환해서 반환한다.
read에서는 그냥 모든 메마른 responsdto 리스트로 만들어 반환한다.
✔️ 1-18 Update, Delete 구현하기
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
if(memoList.containsKey(id)) {
// 해당 메모 가져오기
Memo memo = memoList.get(id);
// memo 수정
memo.update(requestDto);
return memo.getId();
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
// 해당 메모가 DB에 존재하는지 확인
if(memoList.containsKey(id)) {
// 해당 메모를 삭제하기
memoList.remove(id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
이어서 u, d에 해당하는 메서드들도 만들어 봤다.
공통적으로 해당하는 id가 있는지 확인해서 예외 처리를 하였다.
그리고 메모를 수정하고(이는 따로 엔티티 클래스에 update 메서드 만듦) / 메모를 삭제하였다.
✔️ 1-21 SQL 연습하기
인텔리제이에서 다음과 같이 데이터 베이스를 연동할 수 있다.
SQL 문을 인텔리제이에서 작성할 수 있게 된다.
Diagrams -> ShowDiagram으로 ERD를 볼 수도 있다.
✔️ 1-22 JDBC란 무엇일까?
JDBC는 Java Database Connectivity로 DB에 접근할 수 있도록 Java에서 제공하는 API이다.
DB별로 연결, 전달 방법 등이 다를 수 있는데 JDBC에 연결해야하는 DB의 JDBC 드라이버를 제공하면 DB 연결 로직을 변경할 필요없이 DB 변경이 가능하다.
JdbcTemplate: 커넥션 연결, statement 준비 및 실행, 커넥션 종료 등의 반복적이고 중복되는 작업들을 대신 처리해준다.
그러나 이를 사용할 일은 극히 적다고 한다.
대신 ORM이라는 것을 사용한다고 한다.
숙제는 건너 뛰었다 다음에 하기로~
✔️ 2-1 3 Layer Architecture
Controller: 클라이언트의 요청을 받고, 응답한다. 요청의 처리는 Service에게 전담.
Service: 사용자의 요구사항을 처리 ('비즈니스 로직'). DB 저장 및 조회가 필요할 때는 Repository에게 요청한다.
Repository: DB 관리 (연결, 해제, 자원 관리) 한다. DB CRUD 작업을 처리한다.
뭔가 MVP 패턴이랑 비슷한 느낌도 난다.
✔️ 2-2 역할 분리하기
너무 내용이 길어질 것 같아서 코드는 생략!!
위 아키텍쳐에 따라 controller, service, repository로 분리하였다.
✔️ 2-4 메모장 프로젝트의 IoC & DI
private MemoService memoService;
public MemoController(JdbcTemplate jdbcTemplate) {
memoService = new MemoService(jdbcTemplate);
}
private final MemoRepository memoRepository;
public MemoService(JdbcTemplate jdbcTemplate) {
this.memoRepository = new MemoRepository(jdbcTemplate);
}
객체 중복 생성 문제를 해결.
그러나 아직도 클래스 내부에서 객체를 생성하고 있기 때문에 강한 결합이 발생한다.
생성자로 필드를 건내 받을 수 있으면 좋겠지만 이걸 넣어 줄 main 클래스가 없는데 어떻게 해야 DI를 적용할 수 있을까?
✔️ 2-5 IOC Container와 Bean
public MemoController(MemoService memoService) {
this.memoService = memoService;
}
외부에서 객체를 넣어 줄 때에는 Bean으로 등록된 클래스만 넣을 수 있다.
@Component
일반 클래스를 스프링이 관리하는 Bean으로 만드려면 @Component 애너테이션을 사용하면 된다.
이 @SpringBootApplication 안에 있는 @ComponentScan 애너테이션이 스프링을 run할 때 @Component를 달고 있는 클래스들을 싹 찾아서 Bean으로 등록해준다.
이렇게 커피콩(Bean) 모양이 보이면 그 클래스는 Bean으로 등록이 되어있다는 것이다.
참고로 클래스 이름이 카멜 케이스로 바뀌어 IOC Container에 Bean으로 등록이 된다.
원래는 생성자에 @Autowired를 작성해주어야 하는데 현재 버전에서는 생성자가 한 개 뿐일 때는 생략해도 된다.
객체의 불변성을 지키기 위해 생성자 주입으로 만들었다. (필드에 final 사용)
여기서 롬복을 사용해 생성자를 만들 경우에는?
@RequiredArgsConstructor 애너테이션을 사용하고 Bean 클래스 필드에 final 사용하기
@Autowired는 주입을 받는 클래스도, 주입이 되는 클래스도 Bean 클래스여야 한다!!
@Component
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(ApplicationContext context) {
// 1.'Bean' 이름으로 가져오기
MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");
// 2.'Bean' 클래스 형식으로 가져오기
// MemoRepository memoRepository = context.getBean(MemoRepository.class);
this.memoRepository = memoRepository;
}
...
}
이런 경우는 거의 없지만 직접 Bean 객체를 주입하는 방법도 있긴 하니 참고.
3 Layer Annotation
Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 ‘Bean’으로 등록할 때 해당 ‘Bean’ 클래스의 역할을 명시하기위해 사용한다.
- @Controller, @RestController
- @Service
- @Repository
근데 @Controller, @RestController의 차이는 뭐지? 나중에 찾아보자.
✔️ 2-6 JPA란 무엇일까?
데이터 관리를 위해 SQL을 직접 작성하고 수정하는 것은 귀찮다.
ORM : Object-Relational Mapping
- Object : "객체"지향 언어 (자바, 파이썬)
- Relational : "관계형" 데이터베이스 (H2, MySQL)
말 그대로 자바와 DB를 매핑 해주는 도구이다.
JPA는 Java ORM 기술의 대표적인 표준 명세이다.
JPA 는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준이 하이버네이트이다.
즉 JPA 프레임워크는 이 외에도 여럿 있으나 스프링 부트에서는 기본적으로 ‘하이버네이트’ 구현체를 사용 한다.
✔️ 2-7 Entity 이해하기
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id // 테이블의 기본 키 지정
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;
}
애너테이션에 대한 설명을 잘 확인 해보자...
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@GeneratedValue 옵션을 추가하면 기본 키 생성을 DB에 위임할 수 있다.
✔️ 2-8 영속성 컨텍스트란 무엇일까?
Persistence는 ‘객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질’을 이다. Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이라 생각하면 쉽다.
EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제할 수 있다.
EntityManagerFactory를 통해 EntityManager를 만들 수 있는데, 또 EMF(EntityManagerFactor)는 DB에 대한 정보를 persistence.xml 파일에 넣으면 이 파일을 읽어 만들어진다.
public class EntityTest {
EntityManagerFactory emf;
EntityManager em;
@BeforeEach
void setUp() {
emf = Persistence.createEntityManagerFactory("memo");
em = emf.createEntityManager();
}
@Test
void test1() {
}
}
뭔가 딱 SQLD 시험 공부할 때 배웠던 것들이다...
JPA는 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한번에 DB에 요청해 변경을 반영한다.
마치 개발이 오류 없이 끝나야 커밋을 하는 개발자 처럼...
실수로 메모 데이터베이스 자체를 드랍해버려서 자꾸 에러가 나 애를 좀 먹었다;
꼭 테이블을 드랍하자.
✔️ 2-9 영속성 컨텍스트의 기능
1차 캐시
영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있다.
캐시 저장소는 Map 형태로 되어 있으면 key는 기본 키 값(@Id)를 저장하고 value에는 엔티티 객체를 저장한다.
- 저장: em.persist(memo); 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장.
- 조회 - 캐시 저장소에 조회하는 Id가 존재하지 않은 경우: em.find(Memo.class, 1); 호출 했는데 캐시 저장소에 해당 값이 없다면 DB에서 해당 값을 캐시 저장소에 저장하고 Entity 객체를 반환.
- 조회 - 캐시 저장소에 조회하는 Id가 존재하는 경우: em.find(Memo.class, 1); 호출 했는데 캐시 저장소에 해당 값이 있다면 바로 Entity 객체를 반환한다.
- 삭제 - em.remove(memo); 호출 시 삭제할 Entity를 DELETED 상태로 만든 후 트랜잭션 commit 후 Delete SQL이 DB에 요청된다.
1차 캐시의 장점
- 1차 캐시를 사용하여 SELECT 문의 실행 횟수를 확 줄여준다.
- 객체의 동일성을 보장해준다(같은 주소 같은 객체).
쓰기 지연 저장소(ActionQueue)
JPA는 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영한다.
사실 et.commit()을 호출하면 em.flush() 자동으로 호출된다. em.flush는 DB에 요청, 반영하는 역할을 한다.
변경 감지(Dirty Checking)
update의 경우 해당하는 메서드는 없다.
JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장하는데 flush()가 호출 됐을 때 Entity의 현재 상태와 최초 상태를 비교해서 변경이 있을 경우 그 때 업데이트를 요청 한다.
Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요하다.
반대로 SELECT는 없어도 된다.
✔️ 2-10 Entity의 상태
비영속 상태(Transient): new 연산자를 통해 인스턴스화 된 아직 저장되지 않은 Entity 객체.
영속 상태(Managed): em.persist(memo); 를 통해 관리되고 있는 상태.
준 영속 상태(Detached): 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태.
- detach(entity): 특정 엔티티 detach
- clear(): 영속성 컨텍스트 초기화. 내용만 비울 뿐이라 영속성 콘텍스트를 계속 이용 가능.
- close(): 영속성 콘텍스트 종료. 영속성 콘텍스트 계속 이용 불가.
삭제 상태(Removed): em.remove(memo);로 엔티티 삭제.
merge(entity)
비영속 상태를 영속 상태로 바꾸는 메서드.
SQL에서의 MERGE와 동일하게 엔티티가 없으면 생성하고 있으면 수정한다.
@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();
}
여기서 주의할 점은 memo(비영속) 객체의 정보를 가지고 mergedMemo(영속) 객체를 새로 만들었다는 것으로 memo는 계속 비영속 상태이고 mergedMemo는 영속 상태가 된다. 둘이 다른 객체임도 주의하자.
저장 뿐만 아니라 수정이 되는 경우도 동일한 결과가 나온다.
비영속 상태인 원래 엔티티는 값을 병합하는 용도로만 사용 하는 것.
영속성 컨텍스트로 관리되고 있는 영속 상태일 때만 변경 감지가 이루어진다.
더 들을 수 있으면 듣고 싶었는데 오늘 하루에 다 듣기는 무리였던 것 같다!
🌙 오늘을 마치며 🌙
아 정말 힘든 하루였다.
TIL을 쓰면서 이렇게 스크롤이 짧아진 건 처음이다.
빠르게 강의를 들은 만큼 아마 머리에 장기적으로 남는 건 별로 없을 것 같으니 개인 과제를 마치고 중요한 부분, 아직 완벽하게 습득하지 못 한 부분(e.g. Bean, Entity)들은 추가 복습을 해야겠다.
오늘도 고생했다~
'공부 기록 > 내배캠Java_5기' 카테고리의 다른 글
[내배캠][TIL] 22일 차 - 목요일, Mockito? Jacoco? (0) | 2024.05.16 |
---|---|
[내배캠][TIL] 21일 차 - 화요일, 스프링 개인 과제를 해보자 (0) | 2024.05.14 |
[내배캠][TIL] 19일 차 - 금요일, 자바 주차 끝! (0) | 2024.05.10 |
[내배캠][TIL] 18일 차 - 목요일, 발표 준비? (0) | 2024.05.09 |
[내배캠][TIL] 17일 차 - 수요일, 팀프 개선하기 (0) | 2024.05.08 |