Spring RESTful Web4

김기현·2022년 7월 10일
0
post-thumbnail

Java Persistence API 사용

지금까지 만들었던 API는 데이터가 관계형데이터베이스에 저장되어있지 않고 메모리에 저장되어있어 서버를 재실행하면 초기화가 됩니다. JPA(Java Persistence API)를 이용해 관계형데이터베이스와 연동하는 방법을 알아보겠습니다. JDBC 데이터베이스를 직접 사용하는 것이 아닌, JPA를 Java에서 정의한 Object와 데이터베이스에서 사용하는 Entity모델과 매핑하는 방식으로 프로그래밍을 하겠습니다.

Java Persistence API 개요

JPA란 Java Persistence API의 약자로 Java ORM 기술에 대한 API 표준 명세입니다. 인터페이스로 특정한 구현체가 존재하는 것이 아닌 선언문만 존재하는 규칙, 약속입니다.

Hibernate는 JPA를 구현한 대표적인 라이브러리로 재사용성이 좋고, 쉬운 유지보수(직관적)로 인한 생산성, 비종속성의 장점으로 비즈니스 로직에 집중할 수 있는 점이 강점이지만, 복잡한 쿼리를 제어하거나 보다 나은 성능을 위해서는 이 라이브러리는 단점으로 꼽힙니다.

Spring Data JPA
스프링 프레임워크는 Spring Data JPA라는 Module을 제공하고 있습니다. 이 모듈에서 구현되어있는 함수나 인터페이스를 활용할 수 있습니다.

JPA를 사용을 위한 Dependency 추가와 설정

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/뒤에 어떤 데이터 값이 온다 하더라도 허용되도록 합니다. 그리고 csrfframeOptions을 사용하지 않도록 합니다.

@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();
    }

    .
    .
    .

이 과정을 마친다면 다음과 같이 정상적으로 실행될 것입니다.

Entity 설정과 초기 데이터 생성

이번에는 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개가 생성됨을 알 수 있습니다.

JPA Service 구현을 위한 Controller, Repository 생성

단순하게 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로 접속하면 입력된 데이터가 잘 반환된 것을 확인할 수 있습니다.

JPA를 이용한 사용자 목록 조회 - GET HTTP Method

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 기능(어떤 주소로 들어가야 하는지)까지 잘 출력된 것을 볼 수 있습니다.

JPA를 이용한 사용자 추가와 삭제 - POST/DELETE HTTP Method

우선 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();
    }

게시물 관리를 위한 Post Entity 추가와 초기 데이터 생성

이번에는 사용자 관리 어플리케이션에 게시물을 작성할 수 있는 기능을 만들어보겠습니다. 시나리오는 각 사용자는 게시물을 작성할 수 있고, 조회할 수 있다 입니다. 그리고 사용자(필수 데이터)와 포스트(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);

게시물 조회를 위한 Post Entity와 User Entity와의 관계 설정

//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게시물의 목록입니다.

JPA를 이용한 새 게시물 추가 - POST HTTP Method

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에도 잘 업로드 된 것을 확인할 수 있습니다.

profile
피자, 코드, 커피를 사랑하는 피코커

0개의 댓글