본문 바로가기

Dev.BackEnd/Spring Boot

#SLiPP Spring boot, JPA 강의 - 반복주기 4


이 포스팅은 다음 강의를 바탕으로 작성되었습니다.

>> SLiPP 자바 웹 애플리케이션 개발 >>

반복주기 4
학습 목표
쿠키와 세션에 대한 대략적인 이해
로그인 사용자에 대한 접근 제한

1. 로그인 기능 구현
2. 로그인 상태에 따른 메뉴 처리 및 로그아웃
3. 로그인 사용자에 한해 자신의 정보를 수정하도록 수정
4. 질문하기, 질문목록 기능 구현
5. 중복 제거 및 리팩토링
6. 원격 서버에 소스코드 배포



1. 로그인 기능 구현
로그인 기능은 페이지가 바뀌더라도 로그인 상태를 유지하는 것이 중요하다.
어떻게 상태를 유지할 것인가? 기본적으로 웹 애플리케이션인 상태를 저장하는 방법이 없다.

코드를 작성하기 전에 로그인을 구현하는 과정을 살펴보자. 로그인의 흐름은 대략 이렇다.
로그인하려는 아이디를 전달받으면 해당 아이디가 데이터베이스에 존재하는지 조회를 한 다음, 아이디가 존재하면 패스워드를 확인하고 존재하지 않으면 에러페이지를 보여주거나 다시 로그인 페이지로 리다이렉션 시켜준다. 아이디가 존재했다는 가정하에 데이터베이스에서 user 객체를 가져온다. 이 user 객체로부터 password를 가져와서 사용자로부터 넘어온 패스워드와 비교를 해줘야 한다. 이 부분에서도 마찬가지로 두 가지의 값이 같다면 로그인 성공인 것이고 다르다면 로그인 실패인 것이다. 이것이 로그인의 대략적인 흐름이다.

일단, 사용자로부터 전달받은 값을 통해 데이터베이스를 조회하자 userRepository 가 제공하는 findOne 메소드를 사용하여 전달된 사용자 정보를 조회해야 한다. 하지만 이렇게 하게 되면 우리가 기본키로 설정한 값으로만 조회가 가능하다. 우리가 전달받은 값은 userId 값 밖에 없기 때문에 userId 값으로 조회해야 한다. UserRepository 인터페이스에 코드를 추가해줘서 조회가 가능하도록 해야 한다.
1
User findByUserId(String userId);
cs

여기서 메서드 이름에 대한 규칙이 있다!
(Spring-boot는 수많은 자동화를 convention을 통해 제공하는듯 하다.)
findBy에 찾으려고 하는 값을 컨벤션에 맞게 설정해줘야 한다.

데이터베이스에서 조회한 값과 전달받은 값을 비교하면서 상식적인 로그인 로직을 생성하자. 우린 이제 로그인 된 사용자 정보를 어딘가에 저장해야 한다. 이 때 Session 이라는 저장소를 사용한다. 세션에 로그인한 사용자 정보를 저장해두면 된다. Model을 사용하여 view에 데이터를 전달할 때처럼, 메소드에 HttpSession 이라는 인자를 추가로 넣어주자. 이렇게 해주면 Model 에 data를 넣어주었듯이, session에 어떠한 값을 저장할 수 있게 된다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PostMapping(“/user/login”)
public String loginInfo(String userId, String password, HttpSession session){
     User user = userRepository.findByUserId(userId);
     if(user.equals(user.getUserId)){
          return “redirect:/user/login”;
     }
 
     if(!password.equals(user.getPassword)){
          return “redirect:/user/login”;
     }
 
     session.setAttribute(“sessionedUser”, user);
     return “redirect:/“;
}
cs

참고자료



2. 로그인 상태에 따른 메뉴 처리 및 로그아웃
로그인 하기 전에는 로그인 버튼과 회원 가입 버튼이 있어야 하고, 로그인을 하면 두 버튼은 사라지고 로그아웃 버튼과 회원 정보 수정 버튼이 있어야 한다. 당연한 것인데, 이를 구현하기 위해서는 로그인 상태인지 안 된 상태인지를 판단해서, 화면에 보일 것과 보이지 말 것을 결정해야 한다.

우선 템플릿 엔진에서 if문을 사용하여 분기를 나눠주자. mustache - if-else 문법을 사용한다. 그리고 템플릿 엔진에서 session에 접근해야 하는데 약간의 설정이 필요한 듯하다이럴 땐 역시 구글링이 답이다! mustache spring session 

오늘도 어김없이 스택오버플로우에서 그 해답을 찾았다. 스프링 부트 설정을 바꿔줘야 한다! 템플릿 엔진에 session 정보를 전달하는 설정이 default로 false가 되어 있다고 한다. 때문에 우리가 데이터베이스를 설정해줬을 때 수정해줬던 application.properties 파일에 구글링으로 나온 코드를 추가해주면 되겠다!
spring.mustache.expose-session-attributes=true

이제 제대로 나온다. 로그아웃을 해보자.로그아웃하고 메인페이지로 리다이렉션 시키도록 하겠다. 로그아웃을 하려면 세션에 담긴 데이터를 날려버려야 한다. removeAttribute 메소드를 사용하자.




3. 로그인 사용자에 한해 자신의 정보를 수정하도록 수정
모델에 전달되는 데이터 이름과 세션에 전달되는 데이터의 이름을 다르게 해줘야 한다. 템플릿 엔진에서 데이터에 접근할 때, 일정한 key 값으로 접근하게 되는데, 모델에 저장된 데이터 명과 세션에 저장된 데이터 명이 같으면 충돌을 일으키기 때문이다.

약간의 UX 적인 부분을 위한 로직은 다음과 같다.
개인정보수정 페이지로 이동하기 전에 session에서 값을 꺼내 로그인 상태인지를 확인한다. 로그인이 안된 상태라면 로그인 페이지로 리다이렉션 시켜준다. 그리고 로그인 된 유저가 자신의 정보를 수정하는 것인지 다른 사람의 정보를 수정하려는 것인지를 확인하기 위해 id 값을 판단해준다.

Controller 에서는 여러 가지 예외 사항에 대한 처리를 해줘야 한다!
잘못된 접근에 대해서, 또는 악의적인 접근에 대해서 예외 처리를 해줘야 한다!



4. 질문하기 답변하기
우리가 관리해야 할 대상이 회원정보(User 객체)에서 질문글(Question 객체)로 바뀐 것 말고는 아무것도 달라진 것이 없다. 그대로 복습한다고 생각하면 된다. 새로운 html 파일(값을 입력받을 페이지)과 새로운 controller(Question 객체를 control할 controller)를 준비하자. index.html 파일의 a 태그 경로를 재설정해주자. 새로 만든 페이지가 기존의 애플리케이션과 연결될 다리를 만들어주는 것이다. question 정보를 저장할 객체를 만들어서 데이터베이스에 매핑한다. 그리고 이 question을 조작할 인터페이스를 만들어야 한다. 회원정보를 받기위한 User 객체를 설정해줬을 때와 완전히 똑같다는 것을 알 수 있다.
한 가지 주의할 점으로는, 클라이언트로부터 넘어오는 정보를 question 객체에 저장하는데, question 객체에 저장할 인자의 종류가 달라서 생성자를 따로 지정해주는 부분이 있다. 이 때, default constructor를 명시적으로 작성해주어야 한다. (jpa 에는 default constructor 가 있어야 된다고 한다.)
1
2
3
4
5
6
7
public Question(){}
public Question(String writer, String title, String contents) {
    super();
    this.writer = writer;
    this.title = title;
    this.contents = contents;
}
cs




5. Refactoring
코드를 작성하는데 있어서 중복된 코드를 제거하는 것은 정말 중요하다.
기능이 작동한다고 해서, 원하는 UI가 나왔다고 해서 그 다음 기능 구현으로 바로 넘어가면 나중에 어마어마한 후폭풍을 맞게 된다!
여러 중복코드가 산재되어 있어, 한 곳이 잘못되면 여러 군데를 수정해주어야 한다던지, 각종 변수명들이 얽히고 설키면서 의미전달이 약해지게 된다. 잠깐의 귀차니즘이 꽤 아픈 부메랑으로 돌아올 수 있기 때문에 리팩토링을 연습하자!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HttpSessionUtils {
    public static final String USER_SESSION_KEY = "sessionedUser";
 
    public static boolean isLoginUser(HttpSession session) {
        Object sessionedUser = session.getAttribute(USER_SESSION_KEY);
        if (sessionedUser == null) {
            return false;
        }
        return true;
    }
 
    public static User getUserFromSession(HttpSession session) {
        if (!isLoginUser(session)) {
            return null;
        }
        return (User) session.getAttribute(USER_SESSION_KEY);
 
    }
}
cs

>> HttpSession을 통해 이루어지는 작업들 중 중복적으로 일어나는 메소드에 대해서 클래스를 따로 지정해, 중복을 제거하였다!

user가 갖고 있는 변수값을 계속 꺼내서 비교하는 것이 아닌, 객체에게 메시지를 보내 객체에게 일을 시키자. 객체지향적으로 코드를 작성하려면, private으로 캡슐화되어 있는 객체의 변수를 계속 노출시키는 것을 피해야 한다.
패스워드를 보기한다고 예를 들어보자. 지금 들어온 이 패스워드 값이 데이터베이스에서 나온 너가 갖고 있는 패스워드와 같니? 라고 메시지를 보낸다. User 객체가 set,get 메소드만 갖고 있는 것이 아니라, matchPassword 같은 메소드를 추가적으로 더 갖고 있어서, 말을 걸고, 대화를 할 수 있게 되는 것이다.
1
2
3
4
5
6
7
public boolean matchPassword(String newPassword) {
    if (newPassword == null) {
        return false;
    }
 
    return newPassword.equals(password);
}
cs

데이터를 꺼내지 않았을 때의 장점은?
private변수를 외부로 노출시키지 않을 수록 좋은 것이다. 객체한테 많은 일을 시켜야 한다. 객체 지향 개발에서 중요한 부분이다. get, set 메소드는 객체 지향을 해치는 요소인 것이다.



6. 원격 서버에 배포 ( war 파일을 통한 배포 )
static파일들이 포함되어 있을 경우 jar(java archaive) 파일이 아닌 war (web archaive)파일로 배포해야 한다. 모바일 애플리케이션의 경우에는 API만 제공하면 되기 때문에 jar 파일로 배포해도 되지만, 웹 애플리케이션의 경우에는 resource 파일도 제공해야하기 때문에 war 파일로 배포해야한다.

현재 사용하고 있는 서버는 Web Server는 Tomcat 서버이다. 근데 이 Tomcat이 스프링 프로젝트에 내장되어 있기 때문에, war 파일에 묶이게 된다. 이러한 점이 문제가 될 수 있기 때문에 우리가 배포에 사용할 서버에 별도의 Tomcat 서버를 설치한 뒤에, 그곳에 war 파일로 배포해야 한다.

내장된 Tomcat이 아닌 외부에 설치된 Tomcat에 프로젝트를 실행시키기 위해서는 클래스 파일이 하나 더 필요하다.
WebInitialIzer.java code>>
1
2
3
4
5
public class WebInitializer extends SpringBootServletInitializer
@Override
protected SpringApplicatioinBuilder configure(SpringApplicationBuilder builder){
     return builder.sources(MyProjectApplication.class);
}
cs


그리고 pom.xml 파일에 외부 Tomcat을 사용한다는 것을 명시해줘야 한다.
1
2
3
4
5
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-tomcat</artifactId>
   <scope>provided</scope>
</dependency>
cs
이제 원격 서버에 tomcat을 설치하자. jdk 를 다운받았던 것과 마찬가지로 wget 명령어를 사용한다. Tomcat 홈페이지에서 tar.gz 확장자의 download link 주소를 복사하여 설치해준다. 버전인 스프링 프로젝트에서 설정해준 버전으로 다운받도록 하자!

소스 코드의 변동사항을 반영하기 위해 git pull 을 해주고, 빌드를 하자. ( ./mvnw clean package 로 빌드 ) 그 결과, target 폴더에 war 파일이 생성된 것을 확인할 수 있다. tomcat을 이용할 때는 webapps 라는 폴더를 사용하여 배포할 수 있다. 따라서 이 폴더로 war 파일을 가져와야 한다. 일단 ROOT를 삭제해준뒤 war를 가져와서 다시 ROOT 폴더에 넣어준다. ( rm -rf(삭제)와 mv(이동) 라는 linux 명령어를 사용한다. 이 때 옮겨줘야 하는 파일은 .war 파일이 아닌, .war original 파일이 아닌, 아무 확장자도 붙어있지 않은 파일을 ROOT로 넣어줘야 한다. (당연히 .war 파일을 옮겨야하는 줄 알고 열심히 삽질을 했다.)

톰캣 서버를 실행시키는 명령어는  ./startup.sh 이고,
종료시키는 명령어는  ./shutdown.sh 이다.


오늘의 꿀팁 1 !
테스트 코드 삽입하기
import.sql 파일을 사용한다.
스프링 부트 프로젝트에서 resource 자원들이 모여있는 resource root 디렉토리에 해당 파일을 생성한다. 그리고 테스트 용으로 사용자 정보를 insert 해준다. 확장자가 sql 이니 sql문을 사용하여 코드를 작성해주면 된다. 이제는 기능 확인을 위해 매번 회원가입을 하지 않아도 된다!

Error Report>>
이 때 sql 문을 작성할 때, 객체에 멤버 변수를 보고 필드명을 작성했는데, 오류가 발생했다. h2-console 창으로 이동해서 우리가 생성한 테이블이 어떤 필드명으로 만들어졌는지 확인할 필요가 있다. console 창에서 정의되어 있는 필드명으로 쿼리문을 작성해야 한다. 당연한 얘기인데, 아이러니 하게도 멤버 변수 명으로 필드명이 설정될 줄 알았다. console 창에서 확인해본 결과, 객체와 테이블을 매핑하는 과정에서 필드명이 객체의 변수 명으로 그대로 매핑되지 않은 것을 확인할 수 있었다. 이제야 해당 데이터가 제대로 들어갔다.


오늘의 꿀팁 2 !
데이터베이스가 익숙하지 않거나 JPA 사용이 쿼리문을 사용하는 것보다 아직 낯선 사람들을 위해 일정 작업이 수행될 때 마다 해당 쿼리문을 볼 수 있는 설정이 있다. application.properties 파일에 다음과 같은 코드를 추가해주면, 커맨드 창에서 어떠한 쿼리가 실행되었는지를 확인할 수 있다!
1
2
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
cs



반복주기 4. End

항상 좋은 강의 감사합니다!