[Spring Boot 실전]6. 회원가입 기능을 추가해보자
앞서 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을 생성하여 연동하고, 스프링 부트내에서 제공하는 세션 기능까지 구현해보았다.
다음 시간에는 게시글마다 댓글 기능과 추천하기 기능을 구현해도록 하자.