- JPA 개요
- Enity 설정
- H2 Console 사용을 위한 SecurityConfig 파일 수정
- JPA Service를 위한 GET/POST/DELETE 메소드 추가
- Post Entity 추가와 User Entity와의 관계 설정
- Java Persistence API
- 자바 ORM 기술에 대한 API 표준 명세
- 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스
- EntityManager를 통해 CRUD 처리
- JPA의 구현체, 인터페이스를 직접 구현한 라이브러리
- 생산성, 유지보수, 비종속성
- Spring Module
- JPA를 추상화한 Repository 인터페이스 제공
Spring Security설정을 주석 처리
- (
JPA) 를 편리하게 사용하기 위해 잠시 주석처리- 주로
pom.xml파일에서 해당 의존성을 주석 처리하거나 설정 파일에서Spring Security관련 구성을 주석 처리할 수 있습니다.
H2 Console을 사용하기 위해서는pom.xml파일과application.yml파일에 설정을 추가해야 합니다.pom.xml:
<!-- H2 Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>application.yml:
spring: h2: console: enabled: true settings: web-allow-others: true
- User 클래스를 엔티티로 지정하고, 데이터베이스 테이블과 매핑하기 위해 필요한 어노테이션을 추가합니다.
User.java:
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "users") public class User { @Id @GeneratedValue private Integer id; // 추가 필드는 여기에 추가 }
- 여기서
@Entity어노테이션은 해당 클래스가 JPA 엔티티임을 표시합니다.@Table(name = "users")은 테이블의 이름을 지정합니다.@Id는 해당 필드가 기본 키(primary key)임을 나타내며,@GeneratedValue는 기본 키 값이 자동으로 생성되도록 설정합니다.- 추가적인 필드가 있다면
User클래스에 추가하여 해당 필드들도 데이터베이스의 컬럼과 매핑되도록 해야 합니다.
- data.sql 파일을
resources폴더에 추가하여 초기 데이터를 삽입합니다.resources/data.sql:
INSERT INTO users(id, join_date, name, password, ssn) VALUES (90001, NOW(), 'User1', 'test111', '701010-1111111'); INSERT INTO users(id, join_date, name, password, ssn) VALUES (90002, NOW(), 'User2', 'test222', '801111-2222222'); INSERT INTO users(id, join_date, name, password, ssn) VALUES (90003, NOW(), 'User3', 'test333', '901111-1222222'); INSERT INTO post(description, user_id) VALUES ('My first post', 90001); INSERT INTO post(description, user_id) VALUES ('My second post', 90001);
- SecurityConfig 파일을 작성하여 H2 Console의 인증을 무시하도록 설정
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityCustomizer; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring() .requestMatchers(new AntPathRequestMatcher("/h2-console/**")); } @Bean public SecurityCustomizer securityCustomizer(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); http.csrf().disable(); http.headers().frameOptions().disable(); return http.build(); } }
- 이렇게 하면
H2 Console을 사용할 때 인증 없이 접근할 수 있게 되며,
초기 데이터가data.sql을 통해 데이터베이스에 삽입됩니다.
- UserRepository는
JpaRepository를 상속받아 User 엔티티에 대한 기본 CRUD 메소드를 제공합니다.@Repository어노테이션을 사용하여 Spring의 구성 요소로 등록합니다.
UserRepository.java:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Integer> { }
- User 엔티티와 상호작용하는
RESTful컨트롤러를 작성합니다.- 이 컨트롤러는
UserRepository를 주입받아 사용자 데이터를 관리하는 메소드를 정의합니다.UserJPAController.java:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/jpa") public class UserJPAController { private final UserRepository userRepository; @Autowired public UserJPAController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("/users") public List<User> retrieveAllUsers() { return userRepository.findAll(); } }
설명
- UserRepository.java:
UserRepository인터페이스는JpaRepository<User, Integer>를 상속받아User엔티티에 대한 기본CRUD메소드를 제공합니다.@Repository어노테이션을 사용하여Spring의 구성 요소로 등록됩니다.
- UserJPAController.java:
@RestController와@RequestMapping("/jpa")어노테이션을 사용하여RESTful웹 서비스를 정의합니다.UserJPAController클래스는UserRepository를 주입받아 사용자 데이터를 관리합니다.@GetMapping("/users")메소드는 모든 사용자 목록을 반환합니다.
- 이 설정을 통해
Spring Data JPA를 사용하여User엔티티에 대한 기본CRUD기능을 쉽게 구현할 수 있으며,RESTful API를 통해 사용자 데이터를 관리할 수 있습니다.
JpaRepository는Spring Data JPA에서 제공하는 인터페이스로,
JPA를 사용하여 데이터베이스 작업을 수행할 수 있도록 다양한 메소드를 제공합니다.- 이 인터페이스는
PagingAndSortingRepository와CrudRepository를 상속받으며, 기본적인CRUD(Create, Read, Update, Delete)및 페이징, 정렬 기능을 지원합니다.
JpaRepository는 제네릭 타입을 사용하여 엔티티 클래스와 그 기본 키의 타입을 지정합니다.- 이 부분에서
JpaRepository<User, Integer>는 다음을 의미합니다:
- User: 엔티티 클래스의 타입을 지정합니다. 여기서는
User엔티티를 관리합니다.- Integer: 엔티티의 기본 키(
primary key) 타입을 지정합니다. 여기서는User엔티티의 기본 키가Integer타입임을 나타냅니다.
JpaRepository인터페이스는 여러 메소드를 제공하며, 이러한 메소드를 통해 기본적인 데이터베이스 작업을 수행할 수 있습니다. 몇 가지 주요 메소드는 다음과 같습니다:
- save(S entity): 엔티티를 저장하거나 업데이트합니다.
- findById(ID id): 기본 키를 사용하여 엔티티를 조회합니다.
- findAll(): 모든 엔티티를 조회합니다.
- deleteById(ID id): 기본 키를 사용하여 엔티티를 삭제합니다.
- delete(S entity): 엔티티를 삭제합니다.
- existsById(ID id): 기본 키를 사용하여 엔티티가 존재하는지 확인합니다.
- count(): 엔티티의 총 개수를 반환합니다.
UserRepository 인터페이스는 Spring Data JPA 의 기능을 사용하여 User 엔티티에 대한 데이터베이스 작업을 쉽게 수행할 수 있습니다. UserJPAController는 이 리포지토리를 사용하여 RESTful API 를 구현합니다.UserJPAController.java:
@GetMapping("/users/{id}") public ResponseEntity<EntityModel<User>> retrieveUser(@PathVariable int id) { Optional<User> user = userRepository.findById(id); if (!user.isPresent()) { throw new UserNotFoundException("id-" + id); } EntityModel<User> entityModel = EntityModel.of(user.get()); WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers()); entityModel.add(linkTo.withRel("all-users")); return ResponseEntity.ok(entityModel); }
@GetMapping("/users/{id}"):
- 이 어노테이션은
HTTP GET요청이/jpa/users/{id}경로로 올 때retrieveUser메소드를 호출하도록 합니다.{id}는 경로 변수로, 조회할 사용자의ID를 의미합니다.
@PathVariable int id:
- URL 경로에서 전달된 ID 값을 메소드 인자로 매핑합니다.
- 예를 들어,
/jpa/users/1요청이 오면id값은 1이 됩니다.
Optional<User> user = userRepository.findById(id):
userRepository의findById메소드를 사용하여 주어진 ID의 사용자 데이터를 조회합니다.- 이 메소드는 조회된 사용자 데이터를
Optional객체로 반환합니다.
- 존재 여부 확인:
if (!user.isPresent()): 조회된 사용자 데이터가 존재하지 않으면UserNotFoundException예외를 발생시킵니다.UserNotFoundException("id-" + id): 사용자 ID를 포함한 예외 메시지를 생성합니다.- 이 예외는
HTTP 404상태 코드와 함께 사용자에게 "사용자를 찾을 수 없음" 메시지를 반환합니다.
- HATEOAS 사용:
EntityModel<User> entityModel = EntityModel.of(user.get()):HATEOAS를 사용하여 사용자 데이터를 감싸는EntityModel객체를 생성합니다.WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers()): 다른 API 엔드포인트(여기서는 모든 사용자 목록 조회 엔드포인트)에 대한 링크를 생성합니다.entityModel.add(linkTo.withRel("all-users")): 생성된 링크를EntityModel에 추가합니다. 이 링크는 클라이언트에게 모든 사용자 목록 조회 API를 쉽게 탐색할 수 있게 해줍니다.
- ResponseEntity 반환:
return ResponseEntity.ok(entityModel): HTTP 200 상태 코드와 함께EntityModel을 응답으로 반환합니다.- 이는 클라이언트가 요청한 사용자 데이터를 포함하며, 추가로 모든 사용자 목록 조회 링크도 포함합니다.
- 이 메소드는 Spring Data JPA의
findById메소드를 사용하여 특정 사용자를 조회하고, HATEOAS를 통해 관련 링크를 추가하여 응답을 구성합니다. 예외 상황을 처리하여 사용자가 존재하지 않을 경우 적절한 메시지를 반환합니다.
UserJPAController.java:
@DeleteMapping("/users/{id}") public void deleteUser(@PathVariable int id) { userRepository.deleteById(id); }설명
@DeleteMapping("/users/{id}"):
- 이 어노테이션은 HTTP DELETE 요청이
/jpa/users/{id}경로로 올 때deleteUser메소드를 호출하도록 합니다.{id}는 경로 변수로, 삭제할 사용자의 ID를 의미합니다.
@PathVariable int id:
- URL 경로에서 전달된 ID 값을 메소드 인자로 매핑합니다.
userRepository.deleteById(id):
userRepository의deleteById메소드를 사용하여 주어진 ID의 사용자 데이터를 삭제합니다.- 해당 사용자가 존재하지 않는 경우
Spring Data JPA는EmptyResultDataAccessException을 던집니다.
SecurityConfig.java:
import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @EnableWebSecurity public class SecurityConfig { @Bean protected SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { http.csrf(AbstractHttpConfigurer::disable); return http.build(); } }설명
@EnableWebSecurity:
- Spring Security 설정을 활성화합니다.
filterChain메소드:
HttpSecurity객체를 통해 보안 설정을 구성합니다.http.csrf(AbstractHttpConfigurer::disable)를 사용하여 CSRF 보호를 비활성화합니다.http.build()를 호출하여 SecurityFilterChain을 구성합니다.- 이 설정을 통해 DELETE, CREATE, UPDATE 같은 작업이 오류 없이 수행되도록 합니다.
UserJPAController.java:
@PostMapping("/users") public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User savedUser = userRepository.save(user); URI location = ServletUriComponentsBuilder .fromCurrentRequest() .path("/{id}") .buildAndExpand(savedUser.getId()) .toUri(); return ResponseEntity.created(location).build(); }
설명
@PostMapping("/users"):
- 이 어노테이션은 HTTP POST 요청이
/jpa/users경로로 올 때createUser메소드를 호출하도록 합니다.
@Valid @RequestBody User user:
@RequestBody어노테이션은 요청 본문을User객체로 변환합니다.@Valid어노테이션은User객체의 유효성을 검사합니다.
User savedUser = userRepository.save(user):
userRepository의save메소드를 사용하여 새로운 사용자 데이터를 데이터베이스에 저장합니다.- 저장된 사용자 데이터를 반환합니다.
- URI 생성:
ServletUriComponentsBuilder를 사용하여 새로 생성된 사용자의 URI를 만듭니다..path("/{id}"): 경로에 새로 생성된 사용자의 ID를 포함합니다..buildAndExpand(savedUser.getId()).toUri(): 새로 생성된 사용자의 ID를 사용하여 URI를 완성합니다.
ResponseEntity.created(location).build():
- HTTP 201 상태 코드와 함께
Location헤더에 새로 생성된 리소스의 URI를 포함한 응답을 반환합니다.
이 설정을 통해 Spring Data JPA와 Spring Security를 사용하여 사용자 추가와 삭제 기능을 구현하고, 보안 설정을 통해 이러한 작업이 오류 없이 수행되도록 합니다.
먼저, 게시물을 관리하기 위한
Post엔티티 클래스를 정의합니다.Post.java:
import javax.persistence.*; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String description; @ManyToOne(fetch = FetchType.LAZY) @JsonIgnore private User user; }설명
@Entity: 이 클래스가 JPA 엔티티임을 나타냅니다.@Data:Lombok을 사용하여getter,setter,toString,equals,hashCode메소드를 자동으로 생성합니다.@NoArgsConstructor와@AllArgsConstructor: Lombok을 사용하여 기본 생성자와 모든 필드를 인자로 받는 생성자를 자동으로 생성합니다.@Id와@GeneratedValue(strategy = GenerationType.IDENTITY): 기본 키인id필드를 자동으로 생성되도록 합니다.@ManyToOne(fetch = FetchType.LAZY):Post엔티티가User엔티티와 N:1 관계임을 나타냅니다. LAZY 로딩을 사용하여User객체를 지연 로딩합니다.@JsonIgnore: 순환 참조 문제를 방지하기 위해user필드를 JSON 직렬화에서 제외합니다.
User.java:
import javax.persistence.*; import javax.validation.constraints.Past; import javax.validation.constraints.Size; import java.util.Date; import java.util.List; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Size(min = 2, message = "Name은 2글자 이상 입력해 주세요.") private String name; @Past private Date joinDate; private String password; private String ssn; @OneToMany(mappedBy = "user") private List<Post> posts; public User() { } public User(Integer id, String name, Date joinDate, String password, String ssn) { this.id = id; this.name = name; this.joinDate = joinDate; this.password = password; this.ssn = ssn; } // getters and setters }
@OneToMany(mappedBy = "user"):Post엔티티와 1:N 관계를 설정합니다.mappedBy속성은Post엔티티의user필드에 의해 매핑됨을 나타냅니다.
User-Post 관계 설정:
- 사용자(User)와 게시물(Post)은 1:N 관계를 가집니다. 하나의 사용자는 여러 개의 게시물을 가질 수 있습니다.
User엔티티에서는List<Post> posts필드를 통해 관련 게시물들을 참조합니다.Post엔티티에서는User user필드를 통해 게시물의 작성자를 참조합니다.
이와 같이 User와 Post 엔티티를 설정하고 초기 데이터를 추가하면 사용자 관리와 게시물 작성을 포함한 기능을 구현할 수 있습니다.
- 위 코드는 사용자의 ID를 받아 해당 사용자가 작성한 모든 게시물을 조회하는 API를 추가하는 것입니다.
retrieveAllPostsByUser 메소드 설명UserJPAController.java:
@GetMapping("/users/{id}/posts") public List<Post> retrieveAllPostsByUser(@PathVariable int id) { Optional<User> user = userRepository.findById(id); if (!user.isPresent()) { throw new UserNotFoundException("id-" + id); } return user.get().getPosts(); }
@GetMapping("/users/{id}/posts"):
- 이 어노테이션은 HTTP GET 요청이
/jpa/users/{id}/posts경로로 올 때retrieveAllPostsByUser메소드를 호출하도록 합니다.{id}는 경로 변수로, 조회할 사용자의 ID를 의미합니다.
@PathVariable int id:
- URL 경로에서 전달된 ID 값을 메소드 인자로 매핑합니다.
Optional<User> user = userRepository.findById(id):
userRepository의findById메소드를 사용하여 주어진 ID의 사용자 데이터를 조회합니다.- 이 메소드는 조회된 사용자 데이터를
Optional객체로 반환합니다.
- 사용자 확인:
- 사용자가 존재하지 않는 경우
UserNotFoundException을 발생시킵니다.
- 게시물 조회:
- 사용자가 존재할 경우 해당 사용자가 작성한 모든 게시물을 반환합니다.
- 위 코드에서
PostRepository인터페이스는JpaRepository인터페이스를 확장합니다.
PostRepository 인터페이스 설명import kr.co.joneconsulting.myrestfulservice.bean.Post; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRepository extends JpaRepository<Post, Integer> { }
JpaRepository<Post, Integer>:
JpaRepository는 Spring Data JPA에서 제공하는 인터페이스로, CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있도록 도와줍니다.Post엔티티와 연관된 JpaRepository를 정의하며, 첫 번째 제네릭 타입은 엔티티 클래스를, 두 번째 제네릭 타입은 기본 키의 데이터 타입을 나타냅니다.- 이 인터페이스를 통해 Spring Data JPA가 자동으로 필요한 메소드들을 제공합니다. 이는 데이터베이스 조작에 필요한 다양한 메소드들이 포함됩니다.
따라서 위 인터페이스를 사용하여 게시물(Post) 엔티티의 CRUD 작업을 간편하게 수행할 수 있습니다. 이는
createPost메소드에서postRepository.save(post)를 통해 구현된 것처럼, 새로운 게시물을 데이터베이스에 저장할 때 사용됩니다.User user = userOptional.get(); post.setUser(user); postRepository.save(post); URI location = ServletUriComponentsBuilder .fromCurrentRequest() .path("/{id}") .buildAndExpand(post.getId()) .toUri(); return ResponseEntity.created(location).build(); }
createPost 메소드 설명UserJPAController.java:
@PostMapping("/users/{id}/posts") public ResponseEntity<Post> createPost(@PathVariable int id, @RequestBody Post post) { Optional<User> userOptional = userRepository.findById(id); if (!userOptional.isPresent()) { throw new UserNotFoundException("id-" + id); } User user = userOptional.get(); post.setUser(user); postRepository.save(post); URI location = ServletUriComponentsBuilder .fromCurrentRequest() .path("/{id}") .buildAndExpand(post.getId()) .toUri(); return ResponseEntity.created(location).build(); }
@PostMapping("/users/{id}/posts"):
- 이 어노테이션은 HTTP POST 요청이
/jpa/users/{id}/posts경로로 올 때createPost메소드를 호출하도록 합니다.{id}는 경로 변수로, 새로운 게시물을 추가할 사용자의 ID를 의미합니다.
@PathVariable int id:
- URL 경로에서 전달된 ID 값을 메소드 인자로 매핑합니다.
- 사용자 조회:
userRepository의findById메소드를 사용하여 주어진 ID의 사용자 데이터를 조회합니다.- 조회된 사용자 데이터를
Optional객체로 반환합니다.
- 사용자 확인:
- 사용자가 존재하지 않는 경우
UserNotFoundException을 발생시킵니다.
- 게시물 생성:
- 사용자가 존재할 경우 해당 게시물의 작성자로 사용자를 설정합니다.
postRepository의save메소드를 사용하여 새로운 게시물을 데이터베이스에 저장합니다.
- 응답 생성:
- 새로운 게시물의 URI를 생성하여 응답 헤더의
Location에 포함시킵니다.- HTTP 상태 코드 201(생성됨)과 함께 응답을 반환합니다.
REST API의 성숙도 모델은Leonard Richardson이 제안한 모델로,RESTful API가 얼마나RESTful한지를4단계로 평가합니다.- 각 단계는
REST원칙의 적용 정도를 나타내며, 단계가 올라갈수록 더RESTful합니다.- 이 모델은
Level 0에서Level 3까지로 나누어집니다.
- 설명: 이 단계는 RESTful하지 않은 API를 나타냅니다. 시스템은 하나의 HTTP 엔드포인트만 사용하여 모든 기능을 수행합니다.
- 특징:
- 단일 URI 엔드포인트가 모든 요청을 처리합니다.
- 모든 요청이 동일한 HTTP 메서드(보통 POST)로 이루어집니다.
- API 요청은 보통 XML이나 JSON 형태의 페이로드를 포함합니다.
- 예시: 모든 요청을
/api엔드포인트로 보내고, 요청의 동작을 페이로드로 구분하는 방식입니다.POST /api Content-Type: application/json { "action": "getUser", "userId": 1 }
- 설명: 이 단계에서는 리소스를 개념화하여 각각의 리소스를 고유한 URI로 표현합니다.
- 특징:
- 개별 리소스는 각각의 URI로 접근됩니다.
- 여전히 HTTP 메서드는 단일 메서드(주로 POST)로 제한될 수 있습니다.
예시: 각 리소스를 개별 URI로 구분하지만, 요청은 여전히 POST로만 처리됩니다.
POST /users Content-Type: application/json { "action": "getUser", "userId": 1 }
- 설명: 이 단계에서는 HTTP 메서드를 사용하여 다양한 동작을 표현합니다. 즉, CRUD(생성, 읽기, 업데이트, 삭제) 작업을 HTTP 메서드로 구분합니다.
- 특징:
- 리소스는 URI로 식별됩니다.
- HTTP 메서드(GET, POST, PUT, DELETE)를 사용하여 다양한 작업을 수행합니다.
- 요청과 응답에 HTTP 상태 코드를 사용합니다.
- 예시: 각 리소스는 HTTP 메서드를 사용하여 조작됩니다.
GET /users/1 // 사용자 정보 조회 POST /users // 새로운 사용자 생성 PUT /users/1 // 사용자 정보 수정 DELETE /users/1 // 사용자 삭제
설명: 이 단계에서는 클라이언트가 서버의 리소스를 탐색하고 상호작용할 수 있도록 하이퍼미디어 링크를 제공합니다. 이는 클라이언트가 서버의 상태를 이해하고, 리소스의 URI를 하드코딩하지 않아도 되게 합니다.
특징:
- 응답에는 관련 리소스에 대한 링크가 포함되어 있습니다.
- 클라이언트는 이 링크를 따라가며 애플리케이션의 상태를 전이시킬 수 있습니다.
- RESTful 원칙을 완전히 준수합니다.
예시: 각 리소스는 관련된 리소스에 대한 링크를 포함합니다.
GET /users/1 Content-Type: application/json { "id": 1, "name": "John Doe", "links": { "self": "/users/1", "friends": "/users/1/friends", "posts": "/users/1/posts" } } GET /users/1/friends Content-Type: application/json { "friends": [ { "id": 2, "name": "Jane Doe", "links": { "self": "/users/2", "friends": "/users/2/friends", "posts": "/users/2/posts" } } ] }
- Level 3는 가장 RESTful한 형태로, 클라이언트가 서버의 상태를 링크를 통해 탐색하고 상호작용할 수 있게 합니다.