[Spring Data JPA Tutorial] 5. Rest API 디자인

2021. 4. 1. 15:12Spring/Spring Data JPA Tutorial

반응형
  1. 설계
  2. POST. 회원추가
    1. save Query Method 정의
    2. service Method
    3. Controller 에 Post api 추가
    4. api 테스트 - 1
    5. RequestBody를 별도의 DTO 클래스로 변경
      1. validation 스타터 추가
      2. RequestBody 용 DTO 정의
      3. Controller 메서드 수정
      4. Service 메서드 수정
    6. api 테스트 - 2

1. 설계

Rest API를 설계하는 방법을 알아보았으니 이제 리소스를 수정하거나 추가, 삭제하는 api를 만들어봅시다.

Rest API 설계 규칙에 따라, 각 api는 아래와 같이 구성하였습니다.

GET /api/users
GET /api/users/{id}
POST /api/users
PUT /api/users/{id}
DELETE /api/users/{id}

※ 이전 시간에 만들었던 demo 프로젝트에서 이어서 진행합니다.


2. POST. 회원추가

2.1. save Query Method 정의

Spring Data JPA 에서 활용되는 Query Method 구현체는 SimpleJpaRepository 클래스에 정의되어 있습니다.

이전 시간에 생성했던 findById 역시 이 클래스 내에 정의되어있는 메서드입니다.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

  private final EntityManager em;
	...

  @Transactional
  @Override
  public <S extends T> S save(S entity) {

    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
      em.persist(entity);
      return entity;
    } else {
      return em.merge(entity);
    }
  }
}

save 메서드는 내부적으로는 EntityManager를 이용하여 persist 또는 merge를 취합니다.

Repository 인터페이스를 상속한 UserRepository 는 save 메서드의 선언으로 위의 메서드를 사용할 수 있습니다.

public interface UserRepository extends Repository<User, Long> {

	Optional<User> findById(Long id);
	User save(User user);

}

2.2. service Method

Repository에 Query Method를 생성했으니, 이제 서비스 메서드를 생성해봅시다.

UserRepository 에서 생성한 save Query Method를 사용한 메서드를 정의합니다.

이때, 이 메서드를 종료 할 시 insert query가 commit 또는 rollback 되도록 @Transactional 설정을 합니다.

@Transactional
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	...

	@Transactional
	public boolean insert(User user) {
		User u = userRepository.save(user);
		return u.getId() != null ? true : false;
	}

}

2.3. Controller 에 Post api 추가

@PostMapping 설정을 한 api를 추가합니다.

@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {

	private final UserService userService;

	...

	@PostMapping("")
	public Map<String, Object> insert(@RequestBody User user) {
		Map<String, Object> response = new HashMap<>();

		if(userService.insert(user)) {
			response.put("result", "SUCCESS");
		} else {
			response.put("result", "FAIL");
			response.put("reason", "회원 정보 저장 실패");
		}

		return response;
	}

}

별도의 VO 생성 없이 기존에 생성했었던 Entity Class 를 이용하여 메서드를 구성하였습니다.

2.4. api 테스트 - 1

Postman, cURL, HTTPie 등을 이용하여 api 테스트를 해봅니다.

※ 이 포스팅에서는 HTTPie 를 이용하여 api를 테스트를 합니다.

관련 포스팅
HTTPie - 사용자 친화적인 Command-line HTTP client


HTTPie를 이용하여 POST api 테스트를 해보았습니다.

http POST :8989/api/users email=coco@cc.com sex=2 name=코코 birthDate=930101 phoneNumber=01088889999 type=2 password=1

그랬더니 아래와 같은 에러가 나타났습니다.

16 17

null을 허용하지 않은 password값이 null로 들어와서 발생된 에러인데
password에는 1이라는 값을 넣었는데 왜 저런 에러가 발생되었을까요?

그 이유는 바로 RequestBody에 이용된 Entity 클래스의 설정때문입니다.

@DynamicInsert @DynamicUpdate
@Getter @Setter
@Entity
@Table(name = "users", indexes = {@Index(name = "UK_USERS_EMAIL", columnList = "email", unique = true)})
public class User implements Serializable {
	...
	@JsonIgnore
	@Column(nullable = false, length = 150)
	private String password;
}

password 컬럼은 위와같이 nullable = false 설정과 @JsonIgnore 설정이 되어있습니다.

사용자 상세조회 api에서 password 정보를 노출하지않기 위해 @JsonIgnore 설정을 추가했었는데, 이 부분 때문에 RequestBody를 이용할 때에도 사용할 수 없게 되었습니다.

18

password에 값이 들어가 있지 않은 것을 확인할 수 있습니다.


게다가 이 컬럼은 nullable을 허용하지 않기 때문에 save 메서드 실행시 에러가 발생된 것입니다.

이러한 맹점에 의해 Entity 클래스를 RequestBody나 ResponseBody에 활용하는 것은 권장하지 않습니다.


2.5. RequestBody를 별도의 DTO 클래스로 변경

RequestBody에 활용할 DTO 클래스를 별도로 만들어서 이용하도록 합시다.

DTO 클래스를 만들기 전에, 먼저 엔티티 생성 또는 수정시 설정될 데이터들을 확인해봅시다.

@DynamicInsert @DynamicUpdate
@Getter @Setter
@Entity
@Table(name = "users", indexes = {@Index(name = "UK_USERS_EMAIL", columnList = "email", unique = true)})
public class User implements Serializable {

	private static final long serialVersionUID = -4253749884585192245L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(updatable = false, nullable = false, columnDefinition = "INT UNSIGNED")
	private Long id;

	@ColumnDefault(value = "0")
	@Column(nullable = false, length = 1, columnDefinition = "CHAR(1)")
	private String type;

	@Column(nullable = false, length = 100)
	private String email;

	@Column(nullable = false, length = 50)
	private String name;

	@ColumnDefault(value = "1")
	@Column(nullable = false, length = 1, columnDefinition = "CHAR(1)")
	private String sex;

	@Column(nullable = false, length = 6)
	private String birthDate;

	@Column(nullable = false, length = 20)
	private String phoneNumber;

	@JsonIgnore
	@Column(nullable = false, length = 150)
	private String password;

	@Column(nullable = false, columnDefinition = "TINYINT(1) DEFAULT 1")
	private boolean active;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
	private Date createdAt;

	@Temporal(TemporalType.TIMESTAMP)
	@Column(nullable = true, columnDefinition = "TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP")
	private Date updatedAt;

	@PrePersist
	protected void onCreate() {
		createdAt = Timestamp.valueOf(LocalDateTime.now());
		active = true;
	}

	@PreUpdate
	protected void onUpdate() {
		updatedAt = Timestamp.valueOf(LocalDateTime.now());
	}
}

컬럼들 중 id는 MariaDB의 PK 생성 규칙에 따라 자동으로 설정될 값이며, createdAt, updatedAt, active는 onCreate(), onUpdate() 메서드가 자동으로 설정해줄 수 있는 값이니 이 3개의 컬럼은 DTO에서 제외해도 됩니다.

2.5.1. validation 스타터 추가

RequestBody값의 유효성 체크를 도와주는 validation 스타터를 pom.xml에 추가합니다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.5.2. RequestBody 용 DTO 정의

RequestBody로 받을 항목들을 정의합니다.
회원가입시 아래의 정보들은 필수 입력값으로 받을 것이기 때문에 모두 @NotNull 설정을 하였습니다.

import javax.validation.constraints.NotNull;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class UserRequest {
	@NotNull private String type,  email, name,
		sex, birthDate, phoneNumber, password;
}

2.5.3. Controller 메서드 수정

insert 메서드의 RequestBody 클래스를 UserRequest로 변경하고, 해당 클래스에 설정된 validation 체크를 이용하기 위해 @Valid 설정을 추가합니다.

import javax.validation.Valid;
...

@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {

	private final UserService userService;

	@PostMapping("")
	public Map<String, Object> insert(@RequestBody @Valid final UserRequest user) {
		Map<String, Object> response = new HashMap<>();

		if(userService.insert(user)) {
			response.put("result", "SUCCESS");
		} else {
			response.put("result", "FAIL");
			response.put("reason", "이미 등록된 회원 정보입니다.");
		}

		return response;
	}
}

2.5.4. Service 메서드 수정

UserRequest에 들어있는 값을 이용하여 User 엔티티클래스의 인스턴스를 생성하고, 그 인스턴스를 userRepository.save() 메서드를 통해 저장합니다.

email은 Unique Key 설정이 되어있는 컬럼이기 때문에 기존에 등록된 값이 있을 경우 save 과정을 거치면 안됩니다.
저장하기 전에 먼저 입력된 이메일과 일치하는 레코드가 있는지 확인한 후, 없을 경우 save과정을 거칩니다.

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	...

	@Transactional
	public boolean insert(final UserRequest u) {
		if(userRepository.findByEmail(u.getEmail()).isPresent()) {
			return false;
		}
		userRepository.save(User.builder()
				.birthDate(u.getBirthDate()).email(u.getEmail())
				.name(u.getName()).password(u.getPassword())
				.phoneNumber(u.getPhoneNumber()).sex(u.getSex()).build());

		return true;
	}
}
public interface UserRepository extends Repository<User, Long> {

	Optional<User> findById(Long id);
	User save(User user);
	Optional<User> findByEmail(String email);

}

lombok의 @Builder 를 이용하여 User 클래스의 인스턴스를 생성하였습니다.
※ Builder 패턴에 대해 더 자세히 알고 싶다면, Item 2. 생성자 인자가 많을 때는 Builder Pattern 고려하기를 참고해주세요.


2.6. api 테스트 - 2

다시 2.2. 와 동일한 방법으로 api 테스트를 해봅니다.

19

이번에는 정상적으로 동작합니다.

만일, type, email, name, sex, birthDate, phoneNumber, password 중 하나의 값이라도 설정되어있지 않을 경우, 400 에러를 발생시킵니다.

22

GitHub에서 demo 프로젝트(v1.0.2)를 다운받아 볼 수 있습니다.

728x90
반응형