본문 바로가기

뭐라도 공부해보자!!( 이론 )/Web

4. Spring/Spring Boot를 이용한 로그인 페이지 만들기 : 임시 저장소 및 서비스 구현, 테스트

반응형

 시작에 앞서서 해당 실습은 인프런에서 제공되는 무료 강좌에서 다룬 실습입니다. 강좌를 진행하신 튜터 분의 설명을 듣고 싶다면 해당 강좌 링크를 남겨둘테니 들으시기를 바랍니다.(개인적으로는 한 번 듣는 것을 추천드립니다!!)

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

이전 글을 보고 오시 않았다면 한 번 보고 오시는 것을 추천드립니다. 실습의 전체적인 구조에 대한 것이어서 특히 보고 오시는 것을 추천드립니다.

 

https://messy-developing-diary.tistory.com/52

 

3. Spring/Spring Boot를 이용한 로그인 페이지 만들기 : 로그인 페이지 관련 구조 설명

시작에 앞서서 해당 실습은 인프런에서 제공되는 무료 강좌에서 다룬 실습입니다. 강좌를 진행하신 튜터 분의 설명을 듣고 싶다면 해당 강좌 링크를 남겨둘테니 들으시기를 바랍니다.(개인적으

messy-developing-diary.tistory.com

 

 본격적으로 실습을 시작해보자. 가장 먼저 구현할 것은 회원 저장을 위한 단위 클래스와 저장소를 구현하는 것이다. 그 전에 이전 글에서 언급한 클래스 구조를 상기해 보자. 클래스 구조는 다음과 같다.

 

클래스 의존관계

 

 현재 개발을 시작해야하지만, 아직은 어떤 종류의 저장소를 사용할지는 모른다고 가정하였다. 그렇기에 우리는 우선은 인터페이스를 활용하여, 레포지터리 클래스가 어떤 기능을 제공하는 지 정해놓기만 할 것이고, 이를 implement 하여 실제 사용할 레포지 클래스를 구현할 것이다. 

 저장 단위로써 우리는 Member 클래스를 사용할 것이다. 클래스 내 변수는 name, id로 각각 String, Long 타입이다. 이름은 실제로 계정을 등록하는 사용자가 작성해야 하고, id는 시스템에서 사용자를 식별하기 위하여 부여되는 값으로 자동으로 부여될 것이다. 클래스에서 제공하는 메서드는 getter와 setter 정도이다. 코드는 다음과 같다.

 

package com.example.loginpage.domain;

public class Member {

    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

}

 

 레포지터리 인터페이스(MemberRepository)를 구현할 것이다. 공통적으로 모든 레포지터리는 4 가지의 공통된 기능을 제공한다. 이는 다음과 같다.

 

  •  save : 새로운 맴버를 저장소에 등록한다.
  • findById : 아이디를 매개 변수로 받고, 해당 멤버에 대한 정보를 반환한다.
  • findByName : 이름을 매개 변수로 받고, 헤딩 멤버에 대한 정보를 반환한다.
  • findAll : 저장소에 있는 모든 맴버를 반환한다.

 

 코드는 다음과 같다.

 

package com.example.loginpage.repository;

import com.example.loginpage.domain.Member;

import java.util.List;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

 

 인터페이스를 구현하였으니 임시로 사용할 저장소를 만들어 보겠다. 일단은 DB를 사용하지 않고 컴퓨터에 저장하는 방식을 사용할 것이다. 클래스 이름은 MemoryMemberRepository로 MemberRepository를 implement 하였다.

 

package com.example.loginpage.repository;

import com.example.loginpage.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }
    public void clearStore() {
        store.clear();
    }
}

 

 간단하게 설명해보고자 한다. 저장소는 id-Member 해쉬 맵 구조이다. 저장할 때는 자체적으로 저장소가 최근에 등록된 id를 기억하다가 새로운 멤버가 등록되면 해당 멤버의 최근 id + 1을 새로운 id로 설정하고 멤버를 저장하는 식이다. id로 멤버를 찾을 수 있는 기능을 제공하며, 이름으로 멤버를 찾을 수 있는 기능도 제공하는데, 단 이때는 같은 이름의 멤버가 있을 때, 가장 먼저 나온 멤버만을 반환하게 된다. 코드 자체가 그리 어렵지는 않으니, 자바를 아예 모르지 않는 이상 쉽게 이해할 수 있을 것이다.

 이제 임시로 사용할 저장소가 생겼으니 본격적으로 서비스를 구현해 보도록 하자. 우리는 딱 2 가지의 기능만을 제공할 것이다. 하나는 새로운 계정을 등록하는 것(단 같은 이름의 멤버가 이미 저장소에 존재하는 경우 오류 메시지를 보낼 것이다), 둘 째는 이미 등록된 계정들을 모두 반환하는 것, 마지막은 아이디로 저장소의 계정을 검색하는 것이다. 서비스 클래스는 다음과 같다.

 

package com.example.loginpage.service;

import com.example.loginpage.domain.Member;
import com.example.loginpage.repository.MemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member){
        memberRepository.findByName(member.getName()).ifPresent(
                m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

 

 서비스, 저장소는 이제 구현을 하였다. 이제 남은 것은 이들을 제어해야 할 Controller이다. 스프링에서 Controller를 구현하는 것은 쉬운 일이다. 그저 컨트롤러 클래스에 몇 가지 Annotation을 달아주기만 하면 된다. 그림으로 표현하면 다음과 같이 나타낼 수 있다.

 

스프링의 동작(정확히는 html을 반환하면서 벌어지는 일들)

 

 스프링에 대해서는 솔직히 나도 학습을 충분하게 하지는 못하였다. 일단은 이해하는 데까지만 설명해 보겠다. 사실 웹 서비스와 컴퓨터에서 일반적으로 사용하는 어플리케이션은 본질적으로 같다. 내 컴퓨터에 저장되어 있는 데이터들을 가공하여 내 컴퓨터에 표시하는 것과 내 컴퓨터와 지구 반대 편에 있는 컴퓨터가 가지고 있는 데이터를 내 컴퓨터로 가지고 와서 내 컴퓨터에서 가공하여 보는 것은 컴퓨터의 입장에서 하등 차이점이 없다는 것이다. (물론 세부적으로 들어가면 당연히 차이가 있지만 물리적 거리가 컴퓨터에게는 큰 의미가 없다는 것만을 이해해주었으면 한다) 

 여기서부터 시작하자. 실제 우리가 가공된 데이터를 보는 화면을 "클라이언트"(Client)라고 한다. 반대로 클라이언트의 요청에 따라서 필요한 데이터를 가공하고 제공하는 컴퓨터를 "서버"(Server)라고 한다. 컨트롤러가 하는 일은 클라이언트가 보내느 메시지와 서버가 해야할 동작을 연결해주는 것이다. 이 때 메시지는 서버 입장에서는 url이다. url이 쿼리 역할을 한다. 쿼리의 종류에 따라서 동작하는 것도 달라진다. 예를 들어 get 종류의 메시지라면 서버는 html 파일을 반환하여 클라이언트 화면에 그림을 그릴 것이다. 반대로 post 종류의 메시지라면 get과 다르게 특정 파일이나 값을 직접 반환하지는 않지만, 더 많은 데이터를 보낼 수 있을 것이고(또한 보내는 내용도 최소한의 보안될 것이다), 어쨌든 다른 메시지를 발생시켜 서비스를 이어나갈 것이다.

 설명이 충분하다고 느껴지지는 않지만, 우선은 실습이 우선이니 여기까지만 이론적인 부분은 다루기로 한다. 일단 우리가 해야할 일은 메시지에 해당하는 쿼리(url)과 그 쿼리에 대한 동작을 이어주는 것이다. 방법은 간단하다. 우선은 컨트롤러의 역할을 하는 클래스에 @Controller라는 어노테이션을 달아준다. 그 뒤 동작해야하는 기능들을 함수 형태로 정의하고 @Mapping(url)라는 어노테이션을 달아주기만 하면된다. 추가로 스프링은 기본적으로 반환할 때, resource 폴더에서 리턴 값의 이름을 가진 html 파일을 찾아서 반환한다. 코드를 보면 좀 더 이해가 쉬울 것이다. (컨트롤러 - 반환되는 html 파일 순으로 써놓았다.)

 

package com.example.loginpage.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";
    }
}

 

<!--home.html-->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
  <div>
    <h1>Hello Spring</h1>
    <p>회원 기능</p>
    <p>
      <a href="/members/new">회원 가입</a>
      <a href="/members">회원 목록</a>
    </p>
  </div>
</div> <!-- /container -->
</body>
</html>

 

package com.example.loginpage.controller;

import com.example.loginpage.domain.Member;
import com.example.loginpage.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
    }

    @GetMapping(value = "/members/new")
    public String createForm() {
        return "members/createMemberForm";
    }

    @PostMapping(value = "/members/new")
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());

        memberService.join(member);

        return "redirect:/";
    }

    @GetMapping(value = "/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

 

<!--createMemberForm.html-->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
  <form action="/members/new" method="post">
    <div class="form-group">
      <label for="name">이름</label>
      <input type="text" id="name" name="name" placeholder="이름을
입력하세요">
    </div>
    <button type="submit">등록</button>
  </form>
</div> <!-- /container -->
</body>
</html>

 

<!--memberList.html-->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
  <form action="/members/new" method="post">
    <div class="form-group">
      <label for="name">이름</label>
      <input type="text" id="name" name="name" placeholder="이름을
입력하세요">
    </div>
    <button type="submit">등록</button>
  </form>
</div> <!-- /container -->
</body>
</html>

 

 서비스 클래스와 리포지터리 클래스를 서버에서 사용하기 위해서는 우선 빈(Bean)에 등록해주어야 할 필요가 있다. @AutoWired가 일종의 빈에 등록되어 이미 가지고 있는 객체를 그대로 사용하겠다는 의미와 같다. 빈에 등록해서 사용하는 이유는 여러가지 있지만 간단하게 설명하자면 객체를 불필요하게 반복적으로 생성하는 일을 줄이고, 사용하는 객체를 통일화하는 것이다. (생각해보라. 특정 데이터베이스를 참조해야하는 데 엉뚱하게 다른 곳에서 참조를 하고 있으면 얼마나 곤란해질지 말이다!!) 빈을 등록하는 방법 또한  간단하다. @Configuration 어노테이션을 달고 있는 클래스 하나를 만들고 객체를 생성하는 함수에다가 @Bean이라는 어노테이션을 달아주기만 하면 된다. 코드는 다음과 같다.

 

package com.example.loginpage;

import com.example.loginpage.repository.JdbcMemberRepository;
import com.example.loginpage.repository.MemberRepository;
import com.example.loginpage.repository.MemoryMemberRepository;
import com.example.loginpage.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

 

 이 것으로 일단은 일차적인 구현은 완료하였다. 글을 쓰다보니, 간단한 서비스 구현에도 꽤 많은 자바, html 파일들이 필요하다는 것을 알았다. 이 파일들을 적절하게 디렉토리를 이용하여 정리하는 것도 중요하기에 참조할 사진을 올릴테니 실습할 때는 사진처럼 디렉터리를 구성하기를 바란다.(그래야지 높은 확률로 제대로 코드가 동작할 것이다.)

 

이대로 파일들의 위치를 맞추어주어야 한다!

 

 이 글이 도움이 되었기를 바라며 글을 마친다.

반응형