Spring Boot 실전

[Spring Boot 실전]6. 회원가입 기능을 추가해보자

진철 2024. 1. 18. 14:27
728x90
반응형

앞서 AWS의 EC2와 RDS를 이용해서 간단하게 배포를 해보았다.

하지만! 생각보다 현재 만든 게시판이 너무 단순해서 기능을 좀 더 추가하고 배포하는 것이 낫다는 생각이 들어 코드를 조금 수정하고자 한다.

그래서 오늘은 회원가입 기능을 추가하고 세션을 통해 로그인을 유지시키는 코드를 작성해보도록 하겠다.

우선 Article과 유사하게 Controller -> Service -> Repository -> DB 과정으로 DTO, DAO를 구성하기 위해서 각각의 코드를 작성한다.

@Getter
@Setter
@Entity
public class User {
    @Id
    private String id;
    private String name;
    private String password;
    private String nickname;

    public User() {
        this.id = id;
        this.name = name;
        this.password = password;
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "User{"+
                "id="+id+
                ", name="+name+
                ", password="+password+
                ", nickname="+nickname+"}";
    }
}

@Getter
@Setter
public class UserForm {
    private String id;
    private String name;
    private String password;
    private String nickname;
}

@Entity를 통해 JPA를 통해 DB에 접근할 수 있게 하고 User table을 생성한다.

사용자의 ID 값이 Primary Key이다.

public interface UserRepository {
    User save(User user); # 사용자가 회원가입 시, 데이터를 저장하는 메소드
    Optional<User> findById(String id); # 사용자의 아이디를 검색하여 중복 확인을 하는 메소드
    Optional<User> findByName(String name); # 사용자의 이름을 통해 검색하는 메소드
    Optional<User> findByNickname(String nickname); # 사용자의 닉네임을 검색하여 중복 확인을 하는 메소드
    List<User> findAll(); # 모든 사용자를 출력하는 메소드
    void delete(String id); # 사용자의 회원 탈퇴 시, 데이터를 삭제하는 메소드
    Optional<User> findByIDAndPassword(String id, String password); # 로그인 시, 해당 아이디와 비밀번호가 일치하는 지 확인하는 메소드

다음으로 DB와 접근할 수 있는 Repository 인터페이스를 작성해준다.

각 메소드의 기능은 주석을 참고하자.

@Repository
public class JpaUserRepository implements UserRepository{
    private final EntityManager em;

    public JpaUserRepository(EntityManager em) {
        this.em = em;
    }
    @Override
    public User save(User user) {
        em.persist(user);
        return user;
    }

    @Override
    public Optional<User> findById(String id) {
        User user = em.find(User.class, id);
        return Optional.ofNullable(user);
    }
    @Override
    public Optional<User> findByName(String name) {
        List<User> result = em.createQuery("select u from User u where u.name=:name", User.class)
                .setParameter("name", name)
                .getResultList();

        return result.stream().findAny();
    }
    @Override
    public Optional<User> findByNickname(String nickname) {
        List<User> result = em.createQuery("select u from User u where u.nickname=:nickname", User.class)
                .setParameter("nickname", nickname)
                .getResultList();

        return result.stream().findAny();
    }
    @Override
    public List<User> findAll() {
        return em.createQuery("select u from User u", User.class)
                .getResultList();
    }
    @Override
    public void delete(String id) {
        User user = findById(id).orElseThrow(() -> new EntityNotFoundException("User not found"));
        em.remove(user);
    }

    @Override
    public Optional<User> findByIDAndPassword(String id, String password) {
        List<User> result = em.createQuery("select u from User u where u.id=:id and u.password=:password", User.class)
                .setParameter("id", id)
                .setParameter("password", password)
                .getResultList();

        return result.isEmpty() ? Optional.empty() : Optional.of(result.get(0));
    }
}

그리고 Repository 인터페이스를 상속한 RepoImplements 클래스의 메소드를 구체적으로 적어준다.

이 때, @Repository 어노테이션을 통해서 의존성을 부여해주었다.

@Service
@Transactional
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String create(User user) {
        userRepository.save(user);
        return user.getId();
    }

    public List<User> findUsers() {
        return userRepository.findAll();
    }

    public Optional<User> findOne(String id) {
        return userRepository.findById(id);
    }

    public Optional<User> findOneN(String nickname) {
        return userRepository.findByNickname(nickname);
    }

    public Optional<User> findByIDAndPassword(String id, String password) {
        return userRepository.findByIDAndPassword(id, password);
    }
}

Service 클래스이다.

이 역시, @Service 어노테이션을 통해서 의존성을 부여해주었고, @Transactional 어노테이션을 통해서 트랜잭션이 일어남을 표시해주었다.

 

@Controller
public class HomeController {
    @GetMapping("/")
    public String home(Model model, HttpSession session) {
        User user = (User) session.getAttribute("user");

        if (user != null) {
            model.addAttribute("loggedIn", true);
        } else {
            model.addAttribute("loggedIn", false);
        }

        return "home";
    }

}

그리고 HomeController 코드이다.

이 코드는 로그인이 이루어지면 세션으로 해당 정보를 저장하는데, 세션이 있다면 로그인이 되어있는 상태이기 때문에 Home 화면에서 '로그아웃', '내 정보' 버튼이 보이고 그렇지 않다면 '회원가입', '로그인' 버튼이 보이게 "loggedIn" 변수를 통해서 전달한다.

 

<div class="container">
    <div>
        <h1>오늘의 게시판</h1>
        <p>
            <a th:if="${loggedIn}" href="/logout">로그아웃</a>
            <a th:if="${loggedIn}" href="/info">내 정보</a>

            <a th:unless="${loggedIn}" href="/register">회원가입</a>
            <a th:unless="${loggedIn}" href="/login">로그인</a>

            <a th:if="${loggedIn}" href="/articles/new">게시글 작성하기</a>
            <a href="articles/">글 목록</a>
        </p>
    </div>
</div>

Home.html의 부분에서 th 기능을 활용하여 전달할 수 있다.

 

@Controller
public class UserController {
    private final UserService userService;
    private final HttpSession session;

    @Autowired
    public UserController(UserService userService, HttpSession session) {
        this.userService = userService;
        this.session = session;
    }

    @GetMapping("/register")
    public String createForm() {
        return "users/registerForm";
    }

    @PostMapping("/register")
    public String create(UserForm form) {
        User user = new User();
        user.setId(form.getId());
        user.setName(form.getName());
        user.setNickname(form.getNickname());
        user.setPassword(form.getPassword());
        userService.create(user);

        return "redirect:/";
    }

    @GetMapping("/login")
    public String createLoginForm() {
        return "users/loginForm";
    }

    @PostMapping("/login")
    public String login(@RequestParam String id, @RequestParam String password) {
        Optional<User> userOptional = userService.findByIDAndPassword(id, password);

        if (userOptional.isPresent()) {
            User user = userOptional.get();
            session.setAttribute("user", user);
            return "redirect:/";
        } else {
            return "redirect:/login";
        }
    }

    @GetMapping("/logout")
    public String logout() {
        session.invalidate();
        return "redirect:/";
    }

    @GetMapping("/info")
    public String detail(Model model) {
        User user = (User) session.getAttribute("user");

        if (user != null) {
            model.addAttribute("user", user);
            return "users/userInfo";
        } else {
            return "redirect:/login";
        }
    }
}

다음은 사용자의 입력을 받는 UserController 코드이다.

다른 부분은 순조롭게 코드를 완성했지만, 로그인을 유지시키는 부분과 "내 정보"를 보여주는 부분이 가장 많은 애를 먹었다.

 

    @PostMapping("/login")
    public String login(HttpSession session, @RequestParam String id, @RequestParam String password) {
        Optional<User> userOptional = userService.findByIDAndPassword(id, password);

        if (userOptional.isPresent()) {
            User user = userOptional.get();
            session.setAttribute("user", user);
            return "redirect:/";
        } else {
            return "redirect:/login";
        }
    }

우선 스프링 부트에서는 세션을 HttpSession을 통해서 관리할 수 있다.

하지만 기존의 코드에서는 HttpSession 변수를 위와 같이 각 메소드마다 계속 초기화하여서 세션이 저장이 되지 않는 문제점이 발생하였다.

따라서 해당 변수를 전역 변수로 Controller 전체에서 사용가능하게 설정하여 문제를 해결할 수 있었다.

"내 정보"를 보여주는 기능 역시, 해당 메서드는 세션에 저장된 정보를 기반으로 화면을 보여주는 기능이다.

따라서 이렇게 세션이 저장되지 않았기 때문에 문제가 계속 발생하였다.

이 역시도, 전역 변수에 저장된 session에 사용자의 정보가 있다면 해당 정보를 가져와서 화면에 출력하게 코드를 수정하여 문제를 해결할 수 있었다.

물론 로그아웃을 하면 해당 세션을 삭제하기 때문에 로그인이 되지 않은 상태에서는 "내 정보"를 볼 수 없게 코드를 작성하였다.

 

이번 시간에는 로그인 기능을 위해서 새로운 Table을 생성하여 연동하고, 스프링 부트내에서 제공하는 세션 기능까지 구현해보았다.

다음 시간에는 게시글마다 댓글 기능과 추천하기 기능을 구현해도록 하자.

728x90
반응형