지금까지 만들었던 API는 데이터가 관계형데이터베이스에 저장되어있지 않고 메모리에 저장되어있어 서버를 재실행하면 초기화가 됩니다. JPA(Java Persistence API)
를 이용해 관계형데이터베이스와 연동하는 방법을 알아보겠습니다. JDBC 데이터베이스를 직접 사용하는 것이 아닌, JPA를 Java에서 정의한 Object와 데이터베이스에서 사용하는 Entity모델과 매핑하는 방식으로 프로그래밍을 하겠습니다.
JPA
란 Java Persistence API의 약자로 Java ORM 기술에 대한 API 표준 명세입니다. 인터페이스로 특정한 구현체가 존재하는 것이 아닌 선언문만 존재하는 규칙, 약속입니다.
Hibernate
는 JPA를 구현한 대표적인 라이브러리로 재사용성이 좋고, 쉬운 유지보수(직관적)로 인한 생산성, 비종속성의 장점으로 비즈니스 로직에 집중할 수 있는 점이 강점이지만, 복잡한 쿼리를 제어하거나 보다 나은 성능을 위해서는 이 라이브러리는 단점으로 꼽힙니다.
Spring Data JPA
스프링 프레임워크는 Spring Data JPA
라는 Module을 제공하고 있습니다. 이 모듈에서 구현되어있는 함수나 인터페이스를 활용할 수 있습니다.
h2 데이터베이스 사용을 위한 라이브러리 추가 및 설정을 하겠습니다.
//pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
//application.yml
datasource:
url: jdbc:h2:mem:testdb
jpa:
show-sql: true
h2:
console: // 콘솔 실행창 가능하게 하기 위함
enabled: true
/h2-console
이라는 엔드포인트 URI 주소로 이전에 SPring Security를 위해 만들었던 아이디, 비밀번호를 입력합니다. 그러면 다음의 화면이 나타납니다.
디폴트 값인 Generic H2 (Embedded)
를 선택하고, Driver Class는 고정값을, 그리고 JDBC URL은 yml
에서 설정해주었던jdbc:h2:mem:testdb
을 입력합니다.
이 때 Whitelabel Error Page 에러가 나타나면서 This application has no explicit mapping for /error, so you are seeing this as a feedback
이라는 문구가 발생할 수도 있는데요,
이는 데이터베이스가 문제가 있는 것이 아닌, 충분하지 못한 Spring Security 설정 때문입니다. SecurityConfig
라는 클래스에 메서드를 하나 더 오버라이드 합니다.
WebSecurityConfigurerAdapter
가 가지고 있는 메서드를 오버라이드 하기 위해 마우스 마우스 우측>Generate>Override Methods를 선택해 매개변수가 HttpSecurity
인 것을 선택합니다. 그리고 /h2-console/**
로 /h2-console/
뒤에 어떤 데이터 값이 온다 하더라도 허용되도록 합니다. 그리고 csrf
과 frameOptions
을 사용하지 않도록 합니다.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().antMatchers("/h2-console/**").permitAll();
http.csrf().disable();
http.headers().frameOptions().disable();
}
.
.
.
이 과정을 마친다면 다음과 같이 정상적으로 실행될 것입니다.
이번에는 Spring Data JPA
를 이용하여 Entity 설정과 초기 데이터 생성을 하겠습니다.
User 도메인 클래스에 @Entity
라는 annotation을 추가합니다. 이로써 해당 클래스에 이름을 가지고 데이터베이스에 테이블을 생성하고 클래스에 선언되어있는 필드를 정보를 통해 테이블 생성에 필요한 컬럼의 정보로 사용합니다. 그리고 @Id
annotation을 통해 기본키 설정을, @GeneratedValue
를 통해 자동으로 생성되는 키값으로 등록해줍니다.
//restfulwebservice1/user/Userr.java
@Entity
public class Userr {
@Id
@GeneratedValue
private Integer id;
.
.
.
Java 오브젝트만 선언하고, 데이터베이스에 객체와 Entity 매핑되는 작업이 자동으로 되기 때문에, 테이블 생성도 자동으로 되며 데이터 값들도 자바의 오브젝트 값으로 가져올 수 있습니다.
이 때 테이블이 생성되지 않을 수도 있는데, h2 버전 2 이상 테이블이 생성되지 않을 때 User
객체를 다른 이름으로 바꾸면 해결됩니다. 왜냐하면 user가 데이터베이스에서 예약어가 되었기 때문입니다. 저의 경우 User을 Userr로 수정하였습니다.
추가로 application.yml 파일에 아래의 코드를 추가해야 DML 이전에 DDL 구문이 실행됩니다.
jpa:
show-sql: true
defer-datasource-initialization: true
아래는 서버가 실행되면서 발생하는 로그입니다. create table을 할 때 이미 존재하는 테이블이 있느면 DROP하나는 명령어가 보입니다. sequence는 1씩 증가하고 마지막으로 Primary key로 id가 지정된 것도 확인할 수 있습니다.
이번에는 초기 데이터를 생성하겠습니다. 이 작업을 하기 위해 resources
라는 폴더에 data.sql
파일을 생성합니다. 확장자는 꼭 sql이여야만 합니다.
아래의 코드로 초기 데이터를 저장합니다.
insert into Userr values (99901, now(), 'User1', 'Password1', '881102-8811021');
insert into Userr values (99902, now(), 'User2', 'Password2', '881102-8811022');
insert into Userr values (99903, now(), 'User3', 'Password3', '881102-8811023');
insert into Userr values
라고 쿼리가 실행되어 초기 데이터 값 3개가 생성됨을 알 수 있습니다.
단순하게 Repository를 생성하는 것만으로도 CRUD에 관련된 메서드를 사용할 수 있습니다. JpaRepository
를 상속받고, 이 레포지토리에서 사용할 Entity와 Entity에 사용할 기본 키값을 설정하겠습니다.
UserRepository
라는 이름으로 새로운 인터페이스(클래스가 아닌 인터페이스 타입)를 생성합니다. 그리고 Bean의 타입을 데이터베이스와 관련된 @Repository
로 정합니다.
그리고 UserRepository
인터페이스는JpaRepository
라는 인터페이스를 상속받는데, 어떠한 Entity를 다룰지 타입을 지정해야 합니다. 여기서는 Userr 타입과 Userr타입에서 기본 키로 설정되어있는 키가 어떤 데이터 형(Userr에서 id로 등록했던 타입인 Interger)인지 파악합니다.
// restfulwebservice1/user/UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<Userr, Integer> {
}
이 코드를 작성하는 것만으로도 여기에 필요한 CRUD에 관련된 메서드를 작성할 수 있습니다.
아래는 상속받은JpaRepository
인터페이스에서 선언되어있는 메서드들입니다. 이 값들을 기본적으로 사용할 수 있으며 추가정의나 재정의를 할 수 있습니다.
다음으로 JPA 레포지토리를 사용하는 Controller 클래스를 생성하겠습니다. UserJpaController
로 명명합니다. 이 Controller 클래스에 조금전에 선언한 UserRepository
를 의존성 주입을 받아서 사용하겠습니다. UserRepository
를 선언하고 의존성 주입을 위해 @Autowired
annotation을 설정합니다. 다음으로 전체 사용자 메서드를 조회하기 위해 findAll()
메서드를 선언합니다.
// restfulwebservice1/user/UserJpaController.java
@RestController
@RequestMapping("/jpa") // 모든 메서드는 /jpa라는 prefix를 가짐
public class UserJpaController {
@Autowired
private UserRepository userRepository; // UserRepository 선언
@GetMapping("/users")
public List<Userr> retrieveAllUsers() {
return userRepository.findAll();
}
}
http://127.0.0.1:8088/jpa/users로 접속하면 입력된 데이터가 잘 반환된 것을 확인할 수 있습니다.
JpaRepository에서 선언된 메서드인 findById를 이용해 사용자 목록을 조회하겠습니다.
// restfulwebservice1/user/UserJpaController.java
@GetMapping("/users/{id}")
public Userr retrieveUser(@PathVariable int id) {
Userr user = userRepository.findById(id);
}
위의 코드는 오류가 발생합니다. userRepository
는 Userr라는 Entity를 사용할 수 있도록 UserRepository
라는 인터페이스에 선언을 해 둔 상태입니다. 그러나 하나의 데이터를 전달할 때는 잘 되지 않는 것 처럼 보이는데요. 우선 findById
가 어떤 값을 전달하는지 부터 파악하겠습니다. 사진과 같이 Optional이 있음을 알 수 있습니다. 왜냐하면 검색에 따라 존재할 수도 있고 아닐 수도 있기 때문에 선택적인 데이터 값을 반환시켜줍니다. 따라서 아래의 코드와 같이 Optional로 반환값을 받아주면 되고, 매개변수로 데이터 형태 값을 Userr
로 받습니다.
// restfulwebservice1/user/UserJpaController.java
@GetMapping("/users/{id}")
public EntityModel<Userr> retrieveUser(@PathVariable int id) {
Optional<Userr> user = userRepository.findById(id);
// user date 존재 하지 않음
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%S] not found", id));
}
// HATEOAS 기능
EntityModel<Userr> model = EntityModel.of(user.get());
WebMvcLinkBuilder linkTo = WebMvcLinkBuilder
.linkTo(WebMvcLinkBuilder.methodOn(this.getClass())
.retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
return model;
}
HATEOAS 기능(어떤 주소로 들어가야 하는지)까지 잘 출력된 것을 볼 수 있습니다.
우선 Controller에 삭제와 관련된 JPA 기능을 생성하겠습니다. 삭제를 하기 위한 메서드로@DeleteMapping
를 이용합니다.
// restfulwebservice1/user/UserJpaController.java
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
userRepository.deleteById(id);
}
다음으로 새로운 사용자를 추가하는 메서드를 만들어보겠습니다. 상태 코드 값을 반영하기 위해 ResponseEntity
메서드를 사용하며, createUser
메서드를 선언하고 @Valid
를 통해 유효성 검사를 @RequestBody
를 통해 사용자들로 부터 데이터를 JSON 타입으로 받습니다. 그리고 userRepository
에서 save
메서드를 호출하는데, 이 때 user라는 도메인 객체를 전달합니다.
그리고 ServletUriComponentsBuilder
를 통해 생성되는 user의 id값을 자동으로 지정할 수 있도록 합니다.
@PostMapping("/users")
public ResponseEntity<Userr> createUser(@Valid @RequestBody Userr user) {
Userr savedUser = userRepository.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
이번에는 사용자 관리 어플리케이션에 게시물을 작성할 수 있는 기능을 만들어보겠습니다. 시나리오는 각 사용자는 게시물을 작성할 수 있고, 조회할 수 있다 입니다. 그리고 사용자(필수 데이터)와 포스트(optional 데이터) 간의 관계는 1:N 입니다.
Post라는 도메인 클래스를 새롭게 만들고, id라는 기본 키와 게시물을 저장하기 위한 description이라는 필드를 생성합니다. 그리고 어떤 user에 의해 생성되었는지도 추가합니다. 이 때 어떤 user에 의해 작성되었는지에 대한 값을 공개하지 않기 위해 @JsonIgnore
를 추가합니다. 그리고 post data가 로딩되는 시점에 사용자가 필요한 데이터를 가져오겠다는 의미로 LAZY
를 구현하였습니다.
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Post {
@Id
@GeneratedValue
private Integer id;
private String description;
// Post : User => N : 1
@ManyToOne(fetch = FetchType.LAZY) //지연로딩방식
@JsonIgnore // 해당 값은 공개하지 않음
private Userr user;
}
post 객체가 완료된 이후에 Userr 도메인 클래스에서 포스팅 정보를 저장할 수 있는 필드를 만듭니다. List형태로 Post를 선언합니다.
//restfulwebservice1/user/Userr.java
@OneToMany(mappedBy = "user") // user table의 데이터와 매핑
private List<Post> posts;
// sql문 오류 => 마우스 우측 constructor 이용
public Userr(int id, String name, Date joinDate, String password, String string) {
this.id = id;
this.name = name;
this.joinDate = joinDate;
this.password = password;
this.ssn = ssn;
}
초기 데이터 값도 같이 지정하겠습니다. data.sql 파일에 아래와 같이 추가합니다.
insert into Post values (100001, 'test posting', 99901);
insert into Post values (100002, 'test posting2', 99901);
//restfulwebservice1/user/UserJpaController.java
@GetMapping("/users/{id}/posts")
public List<Post> retrieveAllPostByUser(@PathVariable int id) {
// 사용자가 존재할 수도 존재하지 않을 수도 있음
Optional<Userr> user = userRepository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%S] not found", id));
}
// 존재한다면 user가 가지고 있는 getPost를 가져옴
return user.get().getPosts();
}
아래와 같이 99901번의 사용자가 작성한 post게시물의 목록입니다.
Post Entity를 이용해 새로운 게시물을 작성할 수 있는 API를 만들어보겠습니다.
PostRepository
라는 인터페이스를 생성합니다. 그 후 Repository 형태의 Bean 형태로 등록하며 JpaRepository
를 상속받습니다. Post Entity를 관리하기 위해 매개변수를 지정하고 기본키 값으로 Integer로 등록합니다.
//restfulwebservice1/user/PostRepository.java
@Repository
public interface PostRepository extends JpaRepository<Post, Integer> {
}
다음으로 userJpaController에 가서 방금 생성한 create 관련 메서드를 사용할 수 있도록 API를 추가합니다.
.
.
.
// PostRepository 선언
@Autowired
private PostRepository postRepository;
.
.
.
@PostMapping("/users/{id}/posts")
public ResponseEntity<Post> createPost(@PathVariable int id, @RequestBody Post post) {
// user_id도 함께 저장되기 때문
Optional<Userr> user = userRepository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(String.format("ID[%S] not found", id));
}
post.setUser(user.get()); // from @Data
Post savedPost = postRepository.save(post);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedPost.getId())
.toUri();
return ResponseEntity.created(location).build();
}
201 create와 함께 정상적으로 post된 것을 확인할 수 있습니다!
그리고 DB에도 잘 업로드 된 것을 확인할 수 있습니다.