[Spring Boot Tutorial] 3. JPA CRUD

2019. 9. 16. 14:35Spring/Spring Boot Tutorial

300x250
반응형

Spring Data JPA CRUD

이전 게시글
[Spring Boot Tutorial] 1. spring boot 시작하기 + 초기세팅
[Spring Boot Tutorial] 2. MySQL + JPA 설정


이전 시간에서 MySQL database 및 JPA 설정 하는 방법과 간단한 정보 조회하는 방법을 알아보았습니다.
이번 시간에서는 insert, update, delete 하는 방법을 알아볼 것입니다.

API는 아래와 같이 구성합니다.

  1. User 조회 : GET /users/{userId}
  2. User 추가 : POST /users
    1. DTO 및 Controller 메서드 생성
    2. Service
    3. API 테스트
  3. User 수정 : PATCH /users/{userId}
    1. Controller Method 생성
    2. Service 메서드에서 Transaction 처리
    3. API 테스트
  4. User 삭제 : DELETE /users/{userId}
    1. Controller Method 생성
    2. Service
    3. API 테스트
  5. ++ tip : 도메인 클래스 컨버터 사용시 주의 사항

1. User 조회 (GET)

이전 시간에 GET 메서드를 이용한 user 조회 api를 만들었습니다.

@GetMapping("/{id}")
public Map<String, Object> findById(@PathVariable("id") long id) {
	Map<String, Object> response = new HashMap<>();

	Optional<User> oUser = userService.findById(id);
	if(oUser.isPresent()) {
		response.put("result", "SUCCESS");
		response.put("user", oUser.get());
	} else {
		response.put("result", "FAIL");
		response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}

	return response;
}

findById를 이용하여 User 엔티티를 조회했었는데, GET 메서드와 같이 Transaction 사용 없이 간단한 조회만 처리할 경우, 우리는 도메인 클래스 컨버터(Domain class converter)를 이용하여 PK를 이용하여 바로 Entity를 조회할 수 있습니다.

@GetMapping("/{id}")
public Map<String, Object> findById(@PathVariable("id") User user) {
	Map<String, Object> response = new HashMap<>();

	if(user != null) {
		response.put("result", "SUCCESS");
		response.put("user", user);
	} else {
		response.put("result", "FAIL");
		response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}

	return response;
}

도메인 클래스 컨버터 기능을 이용하면 User 엔티티의 PK인 id를 이용하여 컨트롤러의 인자에서 바로 User 엔티티로 변환하여 읽어들일 수 있습니다.


2. User 추가 (회원가입) (POST)

jpa에서 저장 작업은 JpaRepository를 상속한 인터페이스의 save 메서드를 통해 할 수 있습니다.
save메서드는 누적된 변경사항들을 모아서 EntityManager가 처리하도록 해주는데
이때, PK가 설정되어있을 경우에는 merge(), null일 경우에는 persist() 메서드가 호출됩니다.
한마디로, save메서드 하나로 update와 insert를 모두 처리할 수 있습니다.

UserController에 회원가입 메서드를 추가해봅시다.


2.1. DTO 및 Controller 메서드 생성

Post 메서드의 Body에 담길 값들에 관한 DTO 클래스를 생성합니다.
자동 설정될 id, create_timestamp, update_timestamp을 제외한 컬럼들을 필드값들로 정의했습니다.

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

@PostMapping("")
public Map<String, Object> save(@RequestBody UserValue value) {
	Map<String, Object> response = new HashMap<>();

	User user = userService.save(value);
	if(user != null) {
		response.put("result", "SUCCESS");
		response.put("user", user);
	} else {
		response.put("result", "FAIL");
		response.put("reason", "회원 가입 실패");
	}

	return response;
}

UserController에 save 메서드를 추가합니다. @RequestBody를 이용하여 POST 메서드의 body에 담긴 값을 읽어옵니다.
회원가입이 정상적으로 이뤄질 경우에는 result 로 SUCCESS, 그리고 저장된 user도 함께 반환합니다.

이제 userService의 save함수를 정의하러 가야겠죠?


2.2. Service

@Transactional
public User save(UserValue value) {
	User user = new User(value.getType(), value.getEmail(), value.getBirthDate(),           
			value.getName()), value.getPassword(), value.getPhoneNumber(), value.getSex());
	return userRepository.save(user);
}

메서드에 @Transaction 애너테이션을 설정할 시, 메서드 내의 일련의 과정이 오류 없이 끝마칠 경우 commit() 를 호출합니다.
insert 동작은 JpaRepository를 상속한 UserRepository의 save함수로 행할 수 있는데

설정해야할 인자가 많을 경우에는 UserValue 클래스를 이용하여 User 객체를 생성하는 방법은 총 3가지 디자인 패턴이 있습니다.

  1. 생성자 패턴
    • 위와 같이 파라미터 있는 생성자로 인스턴스 생성
  2. 자바빈 패턴
    • NoArgsConstructor로 User 생성 후, setter로 필드를 재정의
  3. Builder 패턴
    • 위의 3가지 방법은 모두 동일하게 동작하지만, 가장 안정적인 패턴.

위의 3가지 패턴 중, 안정성과 가독성에 장점이 많은 Builder 패턴을 이용하는 것을 권장합니다.

※ Item 2. 생성자 인자가 많을 때는 Builder Pattern 고려하기

Lombok을 활용하면 엔티티 클래스에 Builder 디자인 패턴을 쉽게 설정할 수 있습니다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter @Setter
@Entity
@Table(name = "user")
@DynamicUpdate
@DynamicInsert
public class User implements Serializable {

	...

	@Builder
	public User(String type, String name, String email, String sex,
			String birthDate, String phoneNumber, String password) {
		this.type = type;
		this.name = name;
		this.email = email;
		this.sex = sex;
		this.birthDate = birthDate;
		this.phoneNumber = phoneNumber;
		this.password = password;
	}
}

빌더패턴을 이용하여 설정할 필드를 넣은 생성자를 만들고 그 위에 @Builder 애너테이션을 추가하면 자동으로 빌더 기능을 이용할 수 있습니다.

이때 주의해야할 점은, ln 1에 추가한 NoArgsConstructor 애너테이션입니다.
JPA를 이용할 때 엔티티에 파라미터 없는 생성자가 없을 경우 에러를 발생시키기 때문에 반드시 파라미터 없는 생성자를 추가해줘야 합니다.
※ 접근제어자는 최소 protected 이상으로 설정해야 합니다.

물론 롬복을 이용하지 않고 직접 생성자를 설정해도 됩니다. ( protected User() { } )


@Transactional
public User save(UserValue value) {
	User user = User.builder()
			.type(value.getType())
			.email(value.getEmail())
			.birthDate(value.getBirthDate())
			.name(value.getName())
			.password(value.getPassword())
			.phoneNumber(value.getPhoneNumber())
			.sex(value.getSex()).build();

	return userRepository.save(user);
}

빌더패턴으로 다시 정의한 save함수입니다.
각 필드명의 메서드를 이용하여 컬럼들을 설정하기 때문에 실수를 줄일 수 있습니다.

참고로 save 메서드는 JpaRepository에 내장된 기본 메서드이기 때문에 UserRepository에서 새로 정의할 필요가 없습니다.


2.3. API 테스트

GET 메서드는 url 접근만으로 메서드 실행을 할 수 있으나, POST, PUT, PATCH 와 같이 body에 값을 담는 메서드는 API 테스트가 어렵습니다.
Postman이라는 프로그램을 이용하면 웹 api 테스트를 쉽게 할 수 있어요.

postman을 다운받고, 우리가 회원가입 api를 실행해봅시다.

24

정상적으로 user생성을 마쳤다면 위와 같이 response를 내보낼 것입니다.


3. user 수정 (PUT, PATCH)

patch method를 이용하여 user 정보를 수정해봅시다.
PUT 메서드와 PATCH 메서드는 Entity를 갱신할 때 이용하는 method인데,
PUT는 데이터 대량 갱신, PATCH는 데이터 부분 갱신에 이용합니다.
(개념상의 의미의 차이만 있을 뿐이기 때문에 아무 method를 사용해도 됩니다.)

여기서는 PATCH method를 이용하여 user 정보를 수정할 것입니다.

3.1. Controller Method 생성

@PatchMapping("/{id}")
public Map<String, Object> patch(@PathVariable("id") long id, @RequestBody UserValue value) {
	Map<String, Object> response = new HashMap<>();

	if(userService.patch(id, value) > 0) {
		response.put("result", "SUCCESS");
	} else {
		response.put("result", "FAIL");
		response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}

	return response;
}

3.2. Service 메서드에서 Transaction 처리

@Transactional
public int patch(long id, UserValue value) {
	Optional<User> oUser = userRepository.findById(id);
	if(oUser.isPresent()) {
		User user = oUser.get();
		if(StringUtils.isNotBlank(value.getType()))
			user.setType(value.getType());
		if(StringUtils.isNotBlank(value.getEmail()))
			user.setEmail(value.getEmail());
		if(StringUtils.isNotBlank(value.getBirthDate()))
			user.setBirthDate(value.getBirthDate());
		if(StringUtils.isNotBlank(value.getName()))
			user.setName(value.getName());
		if(StringUtils.isNotBlank(value.getPassword()))
			user.setPassword(value.getPassword());
		if(StringUtils.isNotBlank(value.getPhoneNumber()))
			user.setPhoneNumber(value.getPhoneNumber());
		if(StringUtils.isNotBlank(value.getSex()))
			user.setSex(value.getSex());
		userRepository.save(user);
		return 1;
	}
	return 0;
}

DB에 해당 사용자가 존재하는지 여부를 체크하기 위해 isPresent()를 이용하여 분기처리 했습니다.
Service 클래스에 @Transaction 을 설정하였기 때문에 patch 메서드 내에서 조회된 정보인 user는 setter를 이용하여 업데이트 될 정보들이 누적되며, patch메서드 종료시 변경사항이 엔티티에 merge됩니다.


StringUtils유틸을 이용하면 빈문자열이거나 기타 white space가 담긴 문자열에 대한 처리를 간단히 처리할 수 있습니다.

StringUtils.isNotBlank(null)      = false
StringUtils.isNotBlank("")        = false
StringUtils.isNotBlank(" ")       = false
StringUtils.isNotBlank("bob")     = true
StringUtils.isNotBlank("  bob  ") = true

pom.xml에 아래의 라이브러리를 추가하면 이용할 수 있습니다.

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
</dependency>

3.3. API 테스트

27


4. User 삭제 (DELETE)

4.1. Controller Method 생성

@DeleteMapping("/{id}")
public Map<String, Object> delete(@PathVariable("id") long id) {
	Map<String, Object> response = new HashMap<>();

	if(userService.delete(id) > 0) {
		response.put("result", "SUCCESS");
	} else {
		response.put("result", "FAIL");
		response.put("reason", "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
	}

	return response;
}

4.2. Service

@Transactional
public int delete(long id) {
	Optional<User> oUser = userRepository.findById(id);
	if(oUser.isPresent()) {
		userRepository.delete(oUser.get());
		return 1;
	}
	return 0;
}

delete 메서드도 JpaRepository 에 포함된 기본 메서드로, 별도로 추가 설정하지 않고 이용할 수 있습니다.


4.3. API 테스트

28


CRUD api를 모두 만든 후의 demo 프로젝트 구조입니다.

02

이번시간에서는 엔티티를 추가하는 방법과 PK를 이용하여 엔티티를 수정, 삭제하고 도메인클래스 컨버터를 이용하여 컨트롤러에서 바로 User 엔티티를 조회하는 방법을 알아보았습니다.

현재에는 PK를 이용하여 엔티티를 조회하는 방법만 이용하였기 때문에 userRepository.findById(id); 메서드 하나로 엔티티를 모두 조회하였는데요. 사실 우리가 웹 애플리케이션을 만들때에는 보다 복잡한 조회 조건을 설정해야할 때가 많습니다.

뒤에 이어질 포스팅에서는 PK이외의 다른 컬럼을 이용한 조회와 필터 적용 유무에 따른 Dynamic Query를 이용하는 방법도 다룰 예정입니다.
그럼 다음 포스팅에서 만나요!


++ tip

Domain Class Converter는 Entity의 PK를 이용하여 Controller에서 엔티티를 조회합니다.
엔티티를 조회한 시점이 바로 컨트롤러이기 때문에, update나 delete와 같은 엔티티 수정을 하고자 할 때 변경사항이 DB에 반영되지 않는 문제가 있습니다.

@Transaction 설정을 하는 위치는 Service 클래스 내입니다.
Service보다 바깥에 위치한 도메인클래스컨버터로 조회한 정보는 Controller에서 조회한 정보이기 때문에 Service내의 트랜젝션이 정상적으로 작동되지 않습니다.

만일 도메인클래스컨버터로 조회한 엔티티정보를 Service 클래스에서 업데이트 하고 싶다면, 명시적으로 save메서드를 호출해 주거나 (userRepository.save(user);) OSIV 속성을 활성화 해주면 됩니다.


OSIV(Open Session In View)는 트랜젝션 범위를 뷰까지 확장하는 기능으로, Service보다 바깥영역에서 조회한 정보를 수정할 수 있게 해주는 기능입니다.

spring:
  jpa:
    open-in-view: true

application.yml 에서 위와 같은 설정을 통해 활성화 시킬 수 있는데, 비활성화 하는것을 권장합니다.
트랜젝션은 서비스 클래스 내에서 처리하고, 서비스 외의 컨트롤러 및 뷰 화면에서는 엔티티를 수정하지 못하도록 하는 것이 보안적인 이유로 좋습니다.

추가적으로, OSIV를 활성화 할 경우에 뷰화면에서 지연로딩(Lazy Loading)도 지원해줘서 개발 중에 편함을 느낄 것입니다.
하지만, 무분별하게 지연로딩을 허용해주는 것은 뷰화면에서 불필요하게 DB를 조회하는 횟수도 늘어나게 되는 단점도 있기 때문에, 서비스 클래스에서 Join 매핑된 지연로딩 대상 클래스들 중 뷰화면에서 출력해야할 정보만 추출하여 새로운 DTO 클래스로 변환하여 response를 구성하는 것을 권장합니다.

따라서, osiv는 비활성화 하여, 트랜젝션은 Service클래스 내의 @Transaction 설정한 메서드 내에서만 이뤄지도록 하고,
도메인클래스컨버터는 조회시에만 사용하는 것을 추천합니다.

300x250
반응형
  • 프로필사진
    jy2020.08.04 17:26

    패키지를 어떤식으로 정리했는지 패키지 트리도 한번씩 보여주시면 더 좋을것 같아요 :)

  • 프로필사진
    jy2020.08.05 16:52

    userService patch에서 userRepository.save(user); 한줄 빠진것 같아요 ㅠㅠ....... 가 아니라 저는 save 안하면 수정이 안되는데 왜그런건가요 ㅠㅠ

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222020.08.07 10:46 신고

      넵, OSIV관련 이슈때문에 발생된 문제였네요 ㅜㅜ
      게시글에 설명이 부족한 점들이 많아 새로 수정해서 올렸습니다!

  • 프로필사진
    2021.08.01 21:38


    Controller
    @Autowired
    private UserRepository userRepository;
    userRepository.save(new Users("이순신","email","picture", Role.GUEST));

    컨트롤러에서 이런식으로하면 db에 insert가 되는데 ㅠㅠ

    @RequiredArgsConstructor
    @Service
    public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    // 유저 생성 및 수정 서비스 로직
    private Users saveOrUpdate(OAuthAttributes attributes) {
    Users users = userRepository.findByEmail(attributes.getEmail())
    .map(entity -> entity.update(attributes.getUser_name(), attributes.getPicture()))
    .orElse(attributes.toEntity());
    로그인도 되는데 insert가 안되는 이유가 있을까요??ㅠㅠ
    원래 users 데이터값 넣어줘야되는데 데이터가 다 찍어보니까 있지만 그래도 그냥 혹시 몰라서 컨트롤러에서 insert 되던거로 했는데도 안되네용 ㅠ 물론 .save(users) 해도 insert는 안됩니다 ㅠ
    return userRepository.save(new Users("이순신","email","picture", Role.GUEST));

    }
    }

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222021.08.04 17:55 신고

      saveOrUpdate 메서드 위에 @Transactional 애너테이션은 설정했는지 확인부탁드려요!