controller
: 웹 계층service
: 비즈니스 로직, 트랜잭션 처리repository
: JPA를 직접 사용하는 계층, 엔티티 매니저 사용domain
: 엔티티가 모여 있는 계층, 모든 계층에서 사용package hello.board.repository;
import hello.board.domain.User;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager em;
public void save(User user) {
em.persist(user);
}
public User findOne(Long id) {
return em.find(User.class, id);
}
public List<User> findAll() {
return em.createQuery("select u from User u", User.class)
.getResultList();
}
public List<User> findByLoginId(String loginId) {
return em.createQuery("select u from User u where u.loginId = :loginId", User.class)
.setParameter("loginId", loginId)
.getResultList();
}
}
em.createQuery
메서드로 JPQL 작성: 엔티티 이름과 엔티티 객체의 필드 명으로 작성package hello.board.service;
import hello.board.domain.User;
import hello.board.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* 회원 가입
*/
@Transactional
public Long join(User user) {
validateDuplicateLoginId(user); //중복 로그인 아이디 검증
userRepository.save(user);
return user.getId();
}
private void validateDuplicateLoginId(User user) {
List<User> findUsers = userRepository.findByLoginId(user.getLoginId());
if (!findUsers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 아이디입니다.");
}
}
/**
* 회원 전체 조회
*/
public List<User> findUsers() {
return userRepository.findAll();
}
/**
* 회원 단건 조회
*/
public User findOne(Long id) {
return userRepository.findOne(id);
}
}
package hello.board.domain;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String loginId;
private String password;
private String name;
private int age;
@Builder
public User(String loginId, String password, String name, int age) {
this.loginId = loginId;
this.password = password;
this.name = name;
this.age = age;
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
protected
로 설정한다.@Builder
@Builder
을 통해서 사용한다.참고: 실무에서 Lombok 사용법
테스트 요구사항
loginId
)가 있으면 예외가 발생해야 한다.package hello.board.service;
import hello.board.domain.User;
import hello.board.repository.UserRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class UserServiceTest {
@Autowired
UserService userService;
@Test
void 회원가입() {
//given
User user = User.builder().loginId("userA").build();
//when
Long joinId = userService.join(user);
//then
assertThat(user).isEqualTo(userService.findOne(joinId));
}
@Test
void 중복_회원_예외() throws Exception {
//given
User user1 = User.builder().loginId("userA").build();
User user2 = User.builder().loginId("userA").build();
//when
userService.join(user1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> userService.join(user2));
//then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 아이디입니다.");
}
}
User를 id나 loginId로 단건 조회할 경우, 이미 삭제되어 User가 없는 경우에는 null 값이 반환되어 NPE가 발생할 수 있다.
NPE가 발생하지 않도록 하기 위해, 단건 조회할 경우 Optional
을 반환하도록 코드를 수정했다.
참고: 스프링 입문 - Ch 3. 회원 관리 예제 - 백엔드 개발(1)
Optional 제대로 활용하기
자바 Optional 사용법 및 예제
UserRepository
package hello.board.repository;
import hello.board.domain.User;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.Optional;
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager em;
public void save(User user) {
em.persist(user);
}
public Optional<User> findOne(Long id) {
User user = em.find(User.class, id);
return Optional.ofNullable(user);
}
public List<User> findAll() {
return em.createQuery("select u from User u", User.class)
.getResultList();
}
public Optional<User> findByLoginId(String loginId) {
List<User> result = em.createQuery("select u from User u where u.loginId = :loginId", User.class)
.setParameter("loginId", loginId)
.getResultList();
return result.stream().findAny();
}
}
findOne
, findByLoginId
메서드가 Optional
을 반환하도록 수정UserService
package hello.board.service;
import hello.board.domain.User;
import hello.board.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* 회원 가입
*/
@Transactional
public Long join(User user) {
validateDuplicateLoginId(user); //중복 로그인 아이디 검증
userRepository.save(user);
return user.getId();
}
private void validateDuplicateLoginId(User user) {
userRepository.findByLoginId(user.getLoginId())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 아이디입니다.");
});
}
/**
* 회원 전체 조회
*/
public List<User> findUsers() {
return userRepository.findAll();
}
/**
* 회원 단건 조회
*/
public Optional<User> findOne(Long id) {
return userRepository.findOne(id);
}
}
validateDuplicateLoginId
메서드 수정findOne
메서드가 Optional
을 반환하도록 수정UserServiceTest
package hello.board.service;
import hello.board.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class UserServiceTest {
@Autowired
UserService userService;
@Test
void 회원가입() {
//given
User user = User.builder().loginId("userA").build();
//when
Long joinId = userService.join(user);
//then
assertThat(user).isEqualTo(userService.findOne(joinId).orElseThrow());
}
@Test
void 중복_회원_예외() throws Exception {
//given
User user1 = User.builder().loginId("userA").build();
User user2 = User.builder().loginId("userA").build();
//when
userService.join(user1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> userService.join(user2));
//then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 아이디입니다.");
}
}
assertThat(user).isEqualTo(userService.findOne(joinId).orElseThrow());