스프링 부트 게시판 프로젝트 - 4 | 회원 도메인 개발

seren-dev·2022년 8월 17일
0

애플리케이션 아키텍처

  • controller: 웹 계층
  • service: 비즈니스 로직, 트랜잭션 처리
  • repository: JPA를 직접 사용하는 계층, 엔티티 매니저 사용
  • domain: 엔티티가 모여 있는 계층, 모든 계층에서 사용

회원 도메인 개발

회원 리포지토리 개발

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;

@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 작성: 엔티티 이름과 엔티티 객체의 필드 명으로 작성

    참고: [JPA] JPQL Query 정리

회원 서비스 개발

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;

@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);
    }
}

회원 엔티티 수정

User

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)
    • 외부에서 기본 생성자를 통해 엔티티를 생성하지 않도록 하여 객체 생성 시 안전성을 높인다.
    • JPA에서 프록시를 생성을 위해서 기본 생성자를 반드시 하나를 생성해야 하기 때문에, 기본 생성자의 접근 레벨을 protected로 설정한다.
  • @Builder
    • 객체에 대한 생성자를 하나로 두고 그것을 @Builder을 통해서 사용한다.

참고: 실무에서 Lombok 사용법

회원 기능 테스트

테스트 요구사항

  • 회원가입을 성공해야 한다.
  • 회원가입 할 때 같은 아이디(loginId)가 있으면 예외가 발생해야 한다.

UserServiceTest

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 도메인 수정

문제점

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());

0개의 댓글