Spring boot) Spring Data JPA๋กœ CRUD ๊ตฌํ˜„, ๊ฒŒ์‹œํŒ M:N ๊ตฌํ˜„

๊ฐœ๋ฐœํ™˜๊ฒฝ

IntelliJ

Spring boot 2.4.1

Gradle

๊ด€๋ จ๋ชจ๋“ˆ : h2, spring-boot-starter-data-jpa


์‚ฌ์ „์ž‘์—…

์ง€๋‚œ ํฌ์ŠคํŠธ์—์„œ ์—”ํ„ฐํ‹ฐ๊นŒ์ง€๋งŒ ๋งŒ๋“ค์—ˆ๋‹ค. ์—ฌ๊ธฐ์— CRUD ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด์„  ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

๊ฐ„๋‹จํ•˜๊ฒŒ ๋งํ•˜์ž๋ฉด, ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ํ…Œ์ด๋ธ” ํ•˜๋‚˜๋‹น ์—”ํ„ฐํ‹ฐ ํด๋ž˜์Šค ํ•˜๋‚˜์™€ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ ํด๋ž˜์Šค ํ•˜๋‚˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด ๋ชจ๋‘๋ฅผ ์ปจํŠธ๋กคํ•  ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์•ผํ•œ๋‹ค.


๋จผ์ € UserJpaController ๋ผ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ์ด๊ฒŒ ๊ธฐ๋ณธ ํ‹€์ด๋‹ค.

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    @Autowired
    private UserRepository userRepository;

}

๊ทธ๋ฆฌ๊ณ  ์—”ํ„ฐํ‹ฐ์ธ Userํด๋ž˜์Šค๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ์„ฑํ•œ๋‹ค.

@Table๋กœ ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Entity๋กœ ์—”ํ„ฐํ‹ฐ ์„ ์–ธ์„ ํ•ด์ค€๋‹ค.

@Id ๋กœ ๊ธฐ๋ณธํ‚ค๋ฅผ ์„ค์ •ํ–ˆ๋‹ค.

@Data
@NoArgsConstructor
@Table(name = "User_list")
@Entity
public class User {

    @Id
    private Integer id;
    private String name;
    private Date joinDate;
    private String password;
    private String ssn;
}

์‚ฌ์šฉ์ž ์กฐํšŒ ๊ตฌํ˜„ - GET

์ „์ฒด ์‚ฌ์šฉ์ž ์กฐํšŒ

์‚ฌ์ „์ž‘์—…์—์„œ ์ปจํŠธ๋กค๋Ÿฌ์™€ User ์—”ํ„ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฏ€๋กœ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ๋งŒ ๋งŒ๋“ค์–ด์ฃผ๊ณ  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ฝ์–ด์˜ค๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์— ์ถ”๊ฐ€ํ•˜์ž.

๋จผ์ € UserRepository์ด๋‹ค. JpaRepository์— ๋“ค์–ด๊ฐ€๋ณด๋ฉด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋“ค์ด ๋‚˜์™€์žˆ๋‹ค. ์—ฌ๊ธฐ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ ธ๋‹ค๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๊ณ , UserRepository ์•ˆ์—์„œ SQL๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ปค์Šคํ…€๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

}

๋ชจ๋“  ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ฝ์–ด์˜ค๊ธฐ ๋•Œ๋ฌธ์— retrieveAllUsers()์—์„œfindAll()๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

public class UserJpaController {
	...
    @GetMapping("/users")
    public List<User> retrieveAllUsers(){
        return userRepository.findAll();
    }
}

์‹คํ–‰ํ•ด๋ณด๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์ฝ์–ด์˜จ๋‹ค.

image-20210107000649824


๊ฐœ๋ณ„ ์‚ฌ์šฉ์ž ์กฐํšŒ

UserJpaController์— ํŠน์ • ID๋งŒ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ์•ž์˜ Optional์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ƒ์†๋œ ๋ฉ”์„œ๋“œ๋ฅผ ํƒ€๊ณ  ์˜ฌ๋ผ๊ฐ€์„œ findById()๋ฅผ ์ฐพ์•„๋ณด๋ฉด, ํ•˜๋‚˜์˜ ID๋ฅผ ๊ฒ€์ƒ‰ํ•  ๋•Œ ๋ฐ˜ํ™˜๊ฐ’์ด Optional์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

image-20210107003618164

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    ...
    @GetMapping("/users/{id}")
    public User retrieveUser(@PathVariable int id){
        Optional<User> user = userRepository.findById(id);

        return user.get();
    }
}

์ด๋ ‡๊ฒŒ๋งŒ ํ•ด๋„ ๊ฒ€์ƒ‰์€ ์ž˜ ๋œ๋‹ค.

image-20210107003454048


์œ„ ์ฝ”๋“œ์—๋‹ค๊ฐ€ ์ง€๋‚œ๋ฒˆ์— ๊ตฌํ˜„ํ•œ hateoas์™€ id๊ฐ€ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•  ์—๋Ÿฌ๋ฉ”์„ธ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    ...
    @GetMapping("/users/{id}")
    public EntityModel<User> retrieveUser(@PathVariable int id){
        Optional<User> user = userRepository.findById(id);

        if(!user.isPresent()){
            throw new UserNotFoundException(String.format("User ID [%s} not found", id));
        }

        EntityModel<User> model = new EntityModel<>(user.get());
        WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
        model.add(linkTo.withRel("all-user"));

        return model;
    }
}

์ด์ œ ํ•˜์ดํผ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•˜๊ณ , ์—๋Ÿฌ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค.

image-20210107004354131


์‚ฌ์šฉ์ž ์‚ญ์ œ ๊ตฌํ˜„ - DELETE

์‚ญ์ œ๋Š” ์ •๋ง ๊ฐ„๋‹จํ•˜๋‹ค. ๊ฐ’์„ ๋”ฐ๋กœ ๋ฆฌํ„ดํ•  ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฌผ๋ก  ์‚ญ์ œ๊ฐ€ ๋˜์—ˆ๋‹ค๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ๋ฆฌํ„ดํ•˜๋„๋ก ๊ตฌํ˜„ํ•ด๋„ ๋œ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด UserJpaController์— delete ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    ...
    @DeleteMapping("/users/{id}")
    public void deleteUser(@PathVariable int id){
        userRepository.deleteById(id);
    }
}

์‹คํ–‰ํ•ด๋ณด์ž. ๋จผ์ € ์‚ญ์ œํ•˜๊ณ 

image-20210107005152716


์กฐํšŒํ•ด๋ณด๋ฉด id 1์€ ์‚ญ์ œ๋˜์–ด์žˆ๋‹ค.

image-20210107005050070


์‚ฌ์šฉ์ž ์ถ”๊ฐ€ ๊ตฌํ˜„- POST

POST ๋ฉ”์„œ๋“œ๋Š” ์‚ด์ง ๋ณต์žกํ•˜๋‹ค. ๋จผ์ € ์ฝ”๋“œ๋ถ€ํ„ฐ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. @valid๋Š” ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋•Œ๋ฌธ์ด์ง€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋Œ์•„๊ฐ€๊ธดํ•œ๋‹ค.

save() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ savedUser์— ๋ฐ˜ํ™˜์‹œํ‚ค๊ณ  ServletUriComponentsBuilder.fromCurrentRequest() ๋ฅผ ์ด์šฉํ•ด์„œ ํ˜„์žฌ ์ƒ์„ฑ๋  id ๊ฐ’์„.path()์— ๋‹ด๊ณ  ์ด๋ฅผ ํ—ค๋”๊ฐ’์œผ๋กœ buildAndExpand()์— ๋งคํ•‘ํ•ด์„œ ์ „๋‹ฌ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค. ์ด๋ฅผ URI ๋ฐ์ดํ„ฐ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    ...
    @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();
    }
}

๋ฐ˜ํ™˜๊ฐ’์ธ ResponseEntity๋ฅผ ์กฐ๊ธˆ ๋œฏ์–ด๋ณด๋ฉด, ํ—ค๋”์™€ ๋ฐ”๋””๋กœ ์ด๋ฃจ์–ด์ง„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

image-20210107015037580

image-20210107015110789


์ด๋ฅผ ์‹คํ–‰์‹œ์ผœ๋ณด์ž. ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ POST์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ํ™•์ธํ•ด๋ณด๋ฉด ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

image-20210107023838259

image-20210107024127219


Post ์‘์šฉ, M:N ๊ด€๊ณ„ ๋งŒ๋“ค๊ธฐ(๊ฒŒ์‹œํŒ)

๊ฒŒ์‹œํŒ์„ ๋ณด๋ฉด ํ•˜๋‚˜์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฒŒ์‹œ๋ฌผ์˜ ์†Œ์œ ๊ถŒ์„ ๊ฐ–๋Š”๋‹ค. ์ด๋ฅผ JPA๋ฐฉ์‹์œผ๋กœ ํ‘œํ˜„ํ•ด๋ณด์ž.

๋จผ์ € UserPost๋ผ๋Š” ์—”ํ„ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์ž. ์—ฌ๊ธฐ์„œ user ํ•„๋“œ๋ฅผ ๋ณด๋ฉด ๋‘๊ฐœ์˜ ์• ๋„ˆํ…Œ์ด์…˜์ด ์‚ฌ์šฉ๋๋Š”๋ฐ,

@ManyToOne์€ UserPost๋ฅผ ๋‹ค์ˆ˜, ๊ทธ๋ฆฌ๊ณ  ๋ถ€๋ชจ๋กœ ์—ฎ์„ User ์—”ํ„ฐํ‹ฐ๋ฅผ ํ•˜๋‚˜๋กœ ๋ณด๊ณ  ๋งคํ•‘์‹œํ‚จ ๊ฒƒ์ด๋‹ค. ์ฆ‰ User : UserPost = 1 : N ์ด ์„ฑ๋ฆฝํ•œ๋‹ค.

LAZY๋กœ ํ•ญ์ƒ UserPost์˜ user ํ•„๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ๋งŒ ๊ฐ€์ ธ์˜ค๋„๋ก ์„ ์–ธํ–ˆ๋‹ค.

@JsonIgnore๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋…ธ์ถœ๋˜์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserPost {
    @Id
    @GeneratedValue
    private Integer id;

    private String description;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnore
    private User user;

}

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์กด์— ์žˆ๋˜ User ํด๋ž˜์Šค์— ์•„๋ž˜๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ

@OneToMany๋กœ User ํด๋ž˜์Šค๋Š” 1:N์—์„œ 1์„ ๋ช…์‹œํ•œ ๊ฒƒ์ด๋‹ค. posts ํ•„๋“œ์— ๋‹ค์ˆ˜๋ฅผ ์ €์žฅํ•  ์˜ˆ์ •์ด๋‹ˆ List๋กœ ์„ ์–ธํ•œ๋‹ค.

public class User {
    ...
    @OneToMany(mappedBy = "user")
    private List<UserPost> posts;
}

๊ทธ๋ฆฌ๊ณ  UserJpaController์˜ ์ „์ฒด ์‚ฌ์šฉ์ž์กฐํšŒ ๋ฉ”์„œ๋“œ์—์„œ ์‚ด์ง๋งŒ ์ˆ˜์ •ํ•˜์—ฌ ์กฐํšŒ ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
    ...
    @GetMapping("/users/{id}/posts")
    public List<UserPost> retrieveAllPostsByUser(@PathVariable int id){
        Optional<User> user = userRepository.findById(id);

        if(!user.isPresent()){
            throw new UserNotFoundException(String.format("User ID [%s} not found", id));
        }

        return user.get().getPosts();
    }
}

์ด์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์‹คํ–‰ํ•ด๋ณด์ž. UserPost ์—”ํ„ฐํ‹ฐ์— User id๊ฐ€ 1๋ฒˆ์ธ ์‚ฌ๋žŒ์ด ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ์ž„์˜์˜ ๊ฐ’์„ ๋„ฃ์—ˆ๋‹ค.

insert into user_post values(1001, 'jeonghoon first post', 1);
insert into user_post values(1002, 'jeonghoon second post', 1);

image-20210107033425919

image-20210107033324597


ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ์กฐํšŒํ•ด๋ณด๋ฉด ๊ธ€์ด id์— ๋งž๊ฒŒ ์ข…์†๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค!

image-20210107033914384


์ž˜ ์ž‘๋™ํ•˜๊ธด ํ•˜๋Š”๋ฐ ๋งค๋ฒˆ insert๋กœ ๊ธ€์„ ์“ธ ์ˆ˜ ์—†์œผ๋‹ˆ ๊ธ€ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•œ POST ๋ฉ”์„œ๋“œ๋ฅผ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด์ž.

UserPost์˜ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋‹ˆ POST๊ฐ€ ์•ˆ ๋  ๊ฒƒ์ด๋‹ค. ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋งŒ๋“ค์–ด์ฃผ์ž. ๊ทธ๋ฆฌ๊ณ  ์ปจํŠธ๋กค๋Ÿฌ ์ˆ˜์ •


UserPostRepository

@Repository
public interface UserPostRepository extends JpaRepository<UserPost, Integer> {
}

์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋งค์ปค๋‹ˆ์ฆ˜์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. UserPost๋Š” User์˜ id๋ฅผ ์ €์žฅํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๋จผ์ € User์˜ id๋ฅผ ๋ฐ›์•„์˜จ๋‹ค
  2. ๋ฐ›์•„์˜จ id๋ฅผ UserPost์— ์ €์žฅํ•œ๋‹ค.

UserJpaController

@RestController
@RequestMapping("/jpa")
public class UserJpaController {
	...
    @Autowired
    private UserPostRepository userPostRepository;
	...
    @PostMapping("/users/{id}/posts")
    public ResponseEntity<UserPost> createUserPost(@PathVariable int id, @RequestBody UserPost userPost){
        //์‚ฌ์šฉ์ž๋ฅผ ๋จผ์ € ๊ฒ€์ƒ‰ํ•จ. Get ๋ฉ”์„œ๋“œ์—์„œ ๊ธ์–ด์™”๋‹ค.
        Optional<User> user = userRepository.findById(id);
        if(!user.isPresent()){
            throw new UserNotFoundException(String.format("User ID [%s} not found", id));
        }
        userPost.setUser(user.get());

        //๊ฒ€์ƒ‰์œผ๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์œผ๋ฉด savedUserPost์— ๋„˜๊ฒจ์ค€๋‹ค. Post๋ฉ”์„œ๋“œ์—์„œ ๊ธ์–ด์™”๋‹ค.
        UserPost savedUserPost = userPostRepository.save(userPost);

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(savedUserPost.getId())
                .toUri();

        return ResponseEntity.created(location).build();
    }
}

๋‘ ๊ฐœ์˜ ๊ฒŒ์‹œ๋ฌผ์„ ์ž‘์„ฑํ–ˆ๋‹ค.

image-20210107040419339

image-20210107040449299


์กฐํšŒํ•ด๋ณด๋‹ˆ ์ •์ƒ์ ์œผ๋กœ ์ถœ๋ ฅ ๋˜์—ˆ๋‹ค!

image-20210107040517937


Jeonghoon Song
Written by@Jeonghoon Song
BFS๋ณด๋‹จ DFS๋ฅผ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž

GitHub