Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d5890c7
build: Spring web 의존성 추가
bingle625 Oct 26, 2024
92c31db
feat: 일단계 테스트 통과하도록 컨트롤러 구현
bingle625 Oct 26, 2024
f60ca1f
build: 타임리프 의존성 추가
bingle625 Oct 26, 2024
89c9355
feat: 어드민 홈 페이지 연결
bingle625 Oct 26, 2024
bc14e15
feat: 예약 조회 페이지 index
bingle625 Oct 26, 2024
aa53458
feat: 예약리스트 api
bingle625 Oct 26, 2024
e9f1555
test: read api 테스트
bingle625 Oct 26, 2024
dfc3f82
feat: 예약 추가, 삭제 api
bingle625 Oct 27, 2024
156e220
feat: 예약 추가, 취소 시 예외 처리
bingle625 Oct 27, 2024
8963ac0
build: jdbc, h2 의존성 추가
bingle625 Oct 27, 2024
df35ea5
build: h2 의존성 추가
bingle625 Oct 30, 2024
13e16ce
add: jdbc test
bingle625 Oct 31, 2024
aa59507
test: 5단계 테스트
bingle625 Oct 31, 2024
2e8d057
test: 6단계 테스트
bingle625 Oct 31, 2024
09f939f
feat: 예약 조회 기능 데이터베이스 연동
bingle625 Oct 31, 2024
4aec219
feat: 예약 추가/삭제 기능 데이터베이스 연동
bingle625 Oct 31, 2024
d7133d8
add: repository
bingle625 Nov 1, 2024
d41fef9
feat: jpaRepository 이용하도록 코드 변경
bingle625 Nov 1, 2024
8b7ffcd
test: Hello 테스트 Spring Bean
bingle625 Nov 16, 2024
251ca70
test: Hello 테스트 Spring Bean
bingle625 Nov 16, 2024
e311d68
Merge branch 'features/jpa'
bingle625 Nov 16, 2024
2662b29
feat: delete 기능 jpa repository 사용하도록 변경
bingle625 Nov 16, 2024
cfc3c26
modify: 도메인별로 클래스 분리 및 사용하지 않는 jdbcTemplate 의존성 삭제
bingle625 Nov 16, 2024
610e556
feat: Time 엔티티, 스케마 추가 및 TimeApiController 클래스 추가
bingle625 Nov 16, 2024
2318082
feat: Time store, index, delete api 구현
bingle625 Nov 16, 2024
2c735cc
feat: 9단계 기존코드 수정
bingle625 Nov 16, 2024
32c827a
test: 10단계 테스트 ReservationApiController에 jdbcTemplate 관련 의존성 없는지 확인하는 코드
bingle625 Nov 16, 2024
d08d455
refactor: ReservationService, TimeService 분리
bingle625 Nov 16, 2024
a7af10e
feat: 일단계 미션 통과하도록 LoginController 구현
bingle625 Dec 27, 2024
14bc007
feat: jpaRepository를 통해 로그인 정보 가져오도록 수정
bingle625 Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타임리프를 추가하셨군요! 이번 미션에선 사용 안한 것 같은데, 어떤 이유로 쓰셨나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트하다가 지우지 않은 흔적으로 보입니다..;

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
// DB
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'


}

test {
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/jdbctest/Customer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jdbctest;

public class Customer {
private long id;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자바엔 long과 Long이 있습니다. 둘은 무엇이 다르고 각각 어떤 장단점이 있나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapper 클래스의 차이는 null값을 가질 수 있냐, 없냐 로 알고 있는데, 좀더 자세히 알아보고 정리해보겠습니다.

long 과 Long 타입의 차이

1. long은 원시(Primitive) 타입, Long 타입은 참조(Reference) 타입

  • 원시 타입은 변수에 데이터 자체를 저장함.

  • 참조 타입은 변수에 데이터가 저장된 주소를 저장함.

  • 원시타입은 null 할당 불가능, 참조타입은 null값 할당 가능

  • 원시타입은 값이 stack에 저장됨.

  • 참조 타입은 주소가 stack에 저장되고, 실제 데이터는 heap에 저장되기 때문에, 원시타입이 더 성능상의 이점이 있음.

  • 보통 엔티티의 Id 가 Long 타입인 이유는, null 값이 배정될 수 있는 편이 여러모로 편하기 때문

  • 의문이 생긴 부분1 : 출처에서 원시타입은 실제 메모리에 값을 저장함 이라고 했는데, 참조 타입도 실제 메모리에 값을 저장하지 않나?

  • 의문이 생긴 부분2 : heap과 stack?


추가 조사: heap과 스택

자바 프로그램 실행시, JVM(자바가상머신)은 하드웨어로부터 전달받은 메모리를 특정 부분들로 나눈다.

메모리 공간(Runtime Data Area)

image
  • Method(static) 영역
  • Stack 영역
  • Heap 영역
    데이터 타입(자료형)에 따라 각 영역에 나눠서 할당 되게 된다.

자바 변수의 종류

  1. 클래스 변수 (클래스 영역에서 static 붙는 변수)
  2. 인스턴스 변수 (클래스 영역에서 static 아닌 변수, 인스턴스 더이상 참조되지 않으면 GC에서 처리함.)
  3. 지역 변수 (메서드 내에서 선언되고 메서드 수행 끝나면 소멸되는 변수)
  4. 매개 변수 ( 메서드 호출 시 '전달하는 값을 가지고 있고, 선언된 부분 부터 수행이 끝날때까지 유효함)

각 변수의 생성 시기
클래스변수 : 클래스가 메모리에 올라갈 때
인스턴스변수 : 인스턴스가 생성되었을 때
지역변수 / 매개변수 : 위치하고 있는 메서드가 수행되었을 때

Method(static) 영역

  • JVM이 동작해서 클래스가 로딩될 때 생성.
  • JVM이 읽어들인 클래스와 인터페이스 대한 런타임 상수 풀, 멤버 변수(필드), 클래스 변수(Static 변수), 상수(final), 생성자(constructor)와 메소드(method) 등을 저장하는 공간.
  • Method(Static) 영역에 있는 것은 어느곳에서나 접근 가능
  • Method(Static) 영역의 데이터는 프로그램의 시작부터 종료가 될 때까지 메모리에 남아있다. 그래서 static 메모리에 있는 데이터들은 프로그램이 종료될 때까지 어디서든 사용이 가능하다.그러나 static 데이터를 무분별하게 많이 사용할 경우 메모리 부족 현상이 일어날수 있게 된다.
image

Stack 영역

  • 메소드 내에서 정의하는 기본 자료형에 해당되는 지역변수의 데이터 값이 저장되는 공간
  • 메소드가 호출될때 스택 영역에 스택 프레임이 생기고 그안에 메소드를 호출
  • primitive 타입의 데이터(int, double, byte, long, boolean 등) 에 해당되는 지역변수, 매개 변수 데이터 값이 저장 
  • 메소드가 호출 될 때 메모리에 할당되고 종료되면 메모리에서 사라짐
    Stack 은 후입선출 LIFO(Last-In-First-Out) 의 특성을 가지며, 스코프(Scope) 의 범위를 벗어나면 스택 메모리에서 사라진다.

스택 프레임(stack frame)
하나의 메서드에 필요한 메모리 덩어리를 묶어서 스택 프레임 이라고 한다.
하나의 메서드 당 하나의 스택 프레임이 필요하며, 메서드를 호출하기 직전 스택 프레임을 자바 Stack 에 생성하고 메서드를 호출한다.
메서드의 매개 변수, 지역변수, 리턴값을 스택 프레임에 쌓는다.
메서드 호출범위가 종료되면 스택에서 제거된다.(가장 최근에 쌓였으니, 삭제하기도 편할듯)

Heap 영역

  • JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
  • 참조형(Reference Type) 데이터 타입을 갖는 객체(인스턴스), 배열 등이 저장 되는 공간
  • 단, Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수는 stack에 적재
  • Heap 영역은 Stack 영역과 다르게 보관되는 메모리가 호출이 끝나더라도 삭제되지 않고 유지된다.그러다 어떤 참조 변수도 Heap 영역에 있는 인스턴스를 참조하지 않게 된다면, GC(가비지 컬렉터)에 의해 메모리에서 청소된다. => 예시는 상세하나 잘 이해되지 않음.
  • stack은 스레드 갯수마다 각각 생성되지만, heap은 몇개의 스레드가 존재하든 상관없이 단 하나의 heap 영역만 존재

Heap과 Stack의 차이점

  • 힙 메모리는 애플리케이션의 모든 부분에서 사용되며, 반면에 스택 메모리는 하나의 스레드가 실행될 때 사용. 그래서 힙 과 메서드 공간에 저장된 객체는 어디서든지 접근이 가능하지만, 스택 메모리는 다른 스레드가 접근할 수 없다.
  • 언제든지 객체가 생성되면 항상 힙 공간에 저장되며, 스택 메모리는 힙 공간에 있는 객체를 참조만 한다.즉, 스택 메모리는 primitive 타입의 지역변수와 힙 공간에 있는 객체 참조 변수만 갖고 있다.
  • 스택메모리의 생명주기는 매우 짧으며, 힙 메모리는 애플리케이션의 시작부터 끝까지 살아남는다.
  • 자바 코드를 실행할때 따로 -Xms과 -Xmx 옵션을 사용하면 힙 메모리의 초기 사이즈와 최대 사이즈를 조절할 수 있다.
  • 스택 메모리가 가득차면 자바에서는 java.lang.StackOverFlowError를 발생.힙 메모리가 가득차면 java.lang.OutOfMemoryError : Java Heap Space 에러를 발생
  • 스택 메모리 사이즈는 힙 메모리와 비교했을 때 매우 적다. 하지만 스택 메모리는 간단한 메모리 할당 방법(LIFO)를 사용하므로 힙 메모리보다 빠르다. ==> 추가 질문: 힙메모리는 어떤 메모리 할당방법을 사용할까?

출처:

private String firstName;
private String lastName;

public Customer(long id, String firstName, String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
}

public long getId() {
return id;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}
}
41 changes: 41 additions & 0 deletions src/main/java/jdbctest/CustomerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package jdbctest;

import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class CustomerController {
private JdbcTemplate jdbcTemplate;

public CustomerController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

@PostMapping("/customers")
public ResponseEntity<Void> save(@RequestBody Customer customer) {
String sql = "INSERT INTO customers(first_name, last_name) VALUES (?,?)";
jdbcTemplate.update(sql, customer.getFirstName(), customer.getLastName());
return ResponseEntity.ok().build();
}

@GetMapping("/customers")
public ResponseEntity<List<Customer>> list() {
String sql = "select id, first_name, last_name from customers";
List<Customer> customers = jdbcTemplate.query(
sql, (resultSet, rowNum) -> {
Customer customer = new Customer(
resultSet.getLong("id"),
resultSet.getString("first_name"),
resultSet.getString("last_name")
);
return customer;
});
return ResponseEntity.ok().body(customers);
}
}
35 changes: 35 additions & 0 deletions src/main/java/jdbctest/DemoApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package jdbctest;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

public static void main(String args[]) {
SpringApplication.run(DemoApplication.class, args);
}

@Autowired
JdbcTemplate jdbcTemplate;

@Override
public void run(String... strings) throws Exception {

jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
jdbcTemplate.execute("CREATE TABLE customers(id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");

List<Object[]> splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long").stream()
.map(name -> name.split(" "))
.collect(Collectors.toList());

jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);

}
}
14 changes: 14 additions & 0 deletions src/main/java/roomescape/HomeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package roomescape;

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

@Controller
public class HomeController {

@GetMapping("/")
public String index() {
return "home";
}

}
41 changes: 41 additions & 0 deletions src/main/java/roomescape/Reservation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package roomescape;

public class Reservation {

private Long id;
private String name;
private String date;
private String time;

public Reservation(final Long id, final String name, final String date, final String time) {
this.id = id;
this.name = name;
this.date = date;
this.time = time;
}

public Long getId() {
return id;
}

public String getTime() {
return time;
}


public String getName() {
return name;
}

public String getDate() {
return date;
}

public static Reservation toEntity(Reservation reservation, Long id) {
return new Reservation(id, reservation.name, reservation.date, reservation.time);
}

public void setId(final Long id) {
this.id = id;
}
}
71 changes: 71 additions & 0 deletions src/main/java/roomescape/ReservationApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package roomescape;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.exceptions.BadRequestException;

@RestController
public class ReservationApiController {

@Autowired
private JdbcTemplate jdbcTemplate;

@GetMapping("/reservations")
public ResponseEntity<List<Reservation>> getReservations() {
List<Reservation> reservations = jdbcTemplate.query("select * from reservation",
((rs, rowNum) -> new Reservation(rs.getLong("id"), rs.getString("name"),
rs.getString("date"), rs.getString("time"))));
return ResponseEntity.ok().body(reservations);
}

@PostMapping("/reservations")
public ResponseEntity<Reservation> create(@RequestBody Reservation reservation) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 도메인 객체를 그대로 파라미터로 받고 있지만, 이는 view와 도메인이 강결합된 형태에요.
어떻게 하면 이를 분리 할 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO를 사용해서 중간 계층을 하나 추가할 수 있을것 같습니다

if (Objects.isNull(reservation.getDate()) || reservation.getDate().isEmpty() ||

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외처리를 꼼꼼히 하셨군요!!
요렇게 할 수도 있지만, 자바의 @Valid를 활용해 볼 수 있습니다.

한번 찾아보시고 학습해보시는 걸 추천드려요

Objects.isNull(reservation.getTime()) || reservation.getTime().isEmpty() ||
Objects.isNull(reservation.getName()) || reservation.getName().isEmpty()) {
throw new BadRequestException("올바르지 않은 입력입니다.");
}

SimpleJdbcInsert simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate).withTableName(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleJdbcInsert도 익히셨군요. 좋습니다.

지금 SimpleJdbcInsert 객체가 요청이 들어올때마다 생성이 되는 것 같아요.
어떻게 하면 이런 객체의 중복생성을 줄일 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자 주입을 받는 방법을 사용할것 같습니다

"reservation").usingGeneratedKeyColumns("id");

Map<String, Object> map = new HashMap<>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map에서 put으로 계속 만들 수도 있지만, Map.of로 한번에 생성괴 동시에 초기화도 할 수 있습니다.

한번 찾아보시는 걸 추천드려요

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다.
10개가 넘어가는 경우, 다른 방법을 택해야 하는 것(put을 쓰거나, ofEntries 사용) 도 확인했습니다.

https://velog.io/@sangwoo0727/Map.of-%EB%A5%BC-%ED%86%B5%ED%95%9C-Map-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90

map.put("name", reservation.getName());
map.put("date", reservation.getDate());
map.put("time", reservation.getTime());
Number newId = simpleJdbcInsert.executeAndReturnKey(map);
reservation.setId(newId.longValue());

return ResponseEntity.created(URI.create("/reservations/" + newId.longValue()))
.body(reservation);
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
int count = this.jdbcTemplate.queryForObject(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 jdbcTemplate엔 this가 붙고 어디엔 안 붙는 군요. 성재님만의 기준이 있으신가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this가 생략 가능한지 몰랐는데, 생략이 되고 있었군용..
this 키워드에 대한 추가적인 조사를 해볼 수 있었습니다.

https://rorobong.tistory.com/122

"select count(*) from reservation where id = ?", new Object[]{id}, int.class);

if (count == 0) {
throw new BadRequestException("존재하지 않는 예약입니다.");
}

this.jdbcTemplate.update("delete from reservation where id = ?", id);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체를 조회한 뒤에, delete 쿼리를 날리는 군요. 이 경우 쿼리가 두번 나갈 거 같아요.

어떻게 하면 쿼리를 줄이면서, 예외처리도 할 수 있을까요?
update 메서드의 반환값은 뭔가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

영향 받는 row의 개수가 0인점을 이용하면 바로 예외처리를 할 수 있겠네요.
몰랐던 사실인데, 감사합니다.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html#update(java.lang.String)


return ResponseEntity.noContent().build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/ReservationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape;

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

@Controller
public class ReservationController {

@GetMapping("/reservation")
public String index() {
return "reservation";
}
}
12 changes: 12 additions & 0 deletions src/main/java/roomescape/exceptions/BadRequestException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package roomescape.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException{

public BadRequestException(String message) {
super(message);
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# h2-console ??? ??
spring.h2.console.enabled=true
# db url
spring.datasource.url=jdbc:h2:mem:database
10 changes: 10 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP TABLE IF EXISTS reservation;

CREATE TABLE reservation
(
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
date VARCHAR(255) NOT NULL,
time VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);
Loading