설모의 기록
[우아한테크캠프] 6일차 본문
HTTP통신과 Spring을 이용한 데이터 저장
오늘은 기본적인 HTTP통신이 이루어지는 간략한 과정과 h2database 를 이용해 Spring에서 데이터를 관리하는 방법에 대해 학습했습니다.
HTTP통신
브라우저에서 http://hyeooona825.tistory.com/ 를 검색하면 그냥 해당 페이지 하나가 로드되는 것과 같아 보이지만, 실제로는 많은 일이 일어납니다. 위의 그림이 간략하게 나타낸 모습입니다.
1. http://hyeooona825.tistory.com/ 를 입력하면 해당 도메인에 대한 IP주소를 알기 위해 DNS 서버에게 요청합니다. 그림에는 DNS 서버가 한 대이지만, 실제로는 여러 상위 DNS 서버가 있을 수 있습니다.
2. DNS 서버는 요청받은 도메인에 해당하는 IP주소를 client에게 보내줍니다.
3. client는 받은 IP를 이용해 Web 서버에게 필요한 작업을 요청합니다. HTTP통신의 대표적인 4가지 메소드는 GET / POST / PUT / DELETE 이며, 해당하는 메소드 방식으로 Web 서버에게 요청을 보내게 됩니다.
4. Web 서버는 client 가 요청하는 작업을 수행해 응답을 보냅니다.
일반적으로 하나의 페이지를 로드하는 과정에서도 여러 작업이 수행됩니다. http://hyeooona825.tistory.com/ 로 요청을 보내면 Web 서버는 해당하는 html을 보내줍니다. 그러면 client에서 html을 읽으면서 css와 같이 다시 요청이 필요한 자원들을 자동으로 서버에게 GET 요청을 보내 자원을 받습니다. 따라서 간단한 GET요청 한 번에도 여러 번의 통신이 일어날 수 있습니다. 이전 [우아한테크캠프] 5일차 포스팅에서 소개한 UserController 예제가 바로 이 방식입니다.
@Controller
public class UserController {
private List<User> users = new ArrayList<>();
@PostMapping("/users")
public String create(User user) {
users.add(user);
return "redirect:/users";
}
@GetMapping("/users")
public String list(Model model) {
model.addAttribute("users", users);
return "/user/list";
}
}
위의 api를 보면 POST 방식의 /users 와 Get 방식의 /users 가 있습니다.
클라이언트가 POST 방식으로 "ip주소:port번호/users" 요청을 보내면 서버는 users 리스트에 request body에 있는 user 를 add 한 후 "redirect:/users" 를 돌려줍니다. 이는 클라이언트에게 302 응답코드와 함께 Location 속성의 값을 /users 로 보낸 것과 같습니다.
응답을 받은 클라이언트는 302 응답 코드를 보고 'Location 속성이 있겠구나!' 라고 생각하고 Location 속성의 값을 보고 GET 방식으로 서버에게 요청을 보냅니다.
요청을 받은 서버는 users 리스트와 함께 /users/list 경로에 해당하는 list.html 파일을 클라이언트에게 보냅니다.
html 파일을 받은 브라우저는 html 파일을 읽으며 css 파일과 같이 다시 요청이 필요한 파일들을 GET 방식으로 서버에게 다시 요청합니다.
이로써 클라이언트는 CSS와 같은 자원이 적용된 html 파일을 브라우저를 통해 볼 수 있습니다.
따라서 Spring에서는 클라이언트에게 요청이 오면 서버가 확인하는 순서는 다음과 같습니다.
- static 폴더 내부에 요청에 해당하는 파일이 있는지
- Controller에 매핑되어 있는 경로가 있는지
- 없으면 404 응답코드를 보낸다.
HTTP통신의 자세한 내용이 궁금하시다면 MDN사이트를 보시면 좋을 것 같습니다.
JPA + Hibernate + h2database 기초 사용법
오늘은 경량 데이터베이스인 h2database를 사용하는 연습을 했습니다. 사용하는 방법은 아래와 같습니다.
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa') // 1번
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-devtools')
compile('org.hibernate:hibernate-java8') // 2번
compile('pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0')
runtime('com.h2database:h2') // 3번
runtime('net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.assertj:assertj-core:3.10.0')
}
먼저 위와 같이 (1, 2, 3) 의 gradle 을 추가한 후 sync를 맞춰줍니다.
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String userId;
private String password;
private String name;
private String email;
public User() {
}
public User(String userId, String password, String name, String email) {
this.userId = userId;
this.password = password;
this.name = name;
this.email = email;
}
public void setUserId(String userId) {
this.userId = userId;
}
public void setPassword(String password) {
this.password = password;
}
public void setName(String name) {
this.name = name;
}
public void setEmail(String email) {
this.email = email;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
ORM 의 표준인 JPA의 장점은 데이터베이스 테이블을 생성할 때 설계에 대한 고민보다 비즈니스 로직에 더욱 집중할 수 있다는 것입니다. 객체 클래스를 생성하면 그에 매핑되는 테이블을 생성하기 때문입니다.
위와 같이 User 클래스를 생성합니다. 이 때, 컬럼에 대한 제약사항은 어노테이션으로 나타냅니다.
먼저 id 멤버변수를 보면 @Id 를 추가해 기본키임을 나타내고,
@GeneratedValue(strategy = GenerationType.IDENTITY) 로 데이터베이스의 auto increment 를 설정합니다.
기본키 이외의 속성들은 @Column 어노테이션을 통해 제약사항을 나타냅니다. 속성에는 nullable 이외에도 unique, length와 같은 더 많은 속성들이 있으니 찾아보시면 좋을 것 같습니다.
public interface UserRepository extends CrudRepository<User, Long> {
User findByUserId(String userId);
User findByEmail(String email);
User findByUserIdAndEmail(String userId, String email);
}
다음으로 CrudRepository<T, ID>를 상속받은 인터페이스를 생성합니다. 기본적으로 id를 통해 데이터베이스에서 객체를 찾는 쿼리는 findById() 메소드를 이용하시면 됩니다. 하지만 쿼리에서 where 절과 같이 조건이 필요한 경우에는 위와 같이 인터페이스내에 메소드를 작성하셔야 합니다.
예를 들어 findByUserId 는 select * from User where userId = userId 인 쿼리를 실행하는 것과 같습니다. 여러개의 where 조건은 메소드에 And를 작성하면 됩니다.
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/{index}")
public String show(@PathVariable Long id, Model model) {
model.addAttribute("user", getUserByIndex(id));
return "/user/profile";
}
@PostMapping("")
public String create(User user) {
userRepository.save(user);
return "redirect:/users";
}
@GetMapping("")
public String list(Model model) {
model.addAttribute("users", userRepository.findAll());
return "/user/list";
}
private User getUserByIndex(Long index) {
return userRepository.findById(index).orElseThrow(NullPointerException::new);
}
}
다음은 UserController 클래스입니다. 먼저 private 멤버변수로 UserRepository 객체인 userRepository 를 생성합니다. 이 후, 데이터베이스의 접근이 필요한 api에서 userRepository 멤버변수를 이용해 h2database에 접근하시면 됩니다.
getUserByIndex() 메소드를 보시면 userRepository.findById(index) 를 통해 User 를 찾아봅니다. 이 때의 반환형은 Optional 입니다. index 에 해당하는 객체가 없을수도 있기 때문에 orElseThrow() 메소드를 통해 NullPointerException 예외를 던져야 합니다.
이제 클라이언트에서 요청을 보내면 h2database에 저장된 회원정보를 통해 응답을 할 수 있습니다.
앞으로의 TODO
제가 지금까지 공부해보지 않은 개념들을 많이 알게되어 공부할 거리가 많이 생겼습니다. JPA, ORM과 같이 각각의 프레임워크의 개념에 대해 더 자세히 공부해봐야 할 것 같습니다. 지금까지 Node.js 로 서버를 구축했었는데, 또 다른 신선한 방식으로 서버를 구축해보는 경험을 할 수 있어 감사함을 느낍니다.
'일상 > 우아한테크캠프' 카테고리의 다른 글
[우아한테크캠프] 8일차 (0) | 2018.07.12 |
---|---|
[우아한테크캠프] 7일차 (0) | 2018.07.11 |
[우아한테크캠프] 5일차 (0) | 2018.07.07 |
[우아한테크캠프] 4일차 (0) | 2018.07.06 |
[우아한테크캠프] 3일차 (0) | 2018.07.04 |