JAVA

스프링부트 블로그 만들기 – 사용자 경험

blog app

스프링부트 블로그 만들기 – 사용자 경험

Alert 창 띄우기

유효성 검사까지 하고 나면 핵심기능은 잘 구현이 된다. 하지만 아래 사진과 같이 회원가입 기능에서 유효성 에러가 났을 경우 사용자에게 개발자용 에러가 표시된다. 이는 사용자 경험(UX)에서 좋지 않으므로 부가기능을 추가해 주는 것이 좋다.

DataintegerityViolationException

프로그램을 만들 때 핵심기능을 만들기 전후로 부가기능을 추가해줘야 한다. 핵심기능을 부가기능과 분리시켜 함수로 만들어 재사용하면 아주 편하게 코딩을 할 수 있다. 하지만 함수로 만들기 위한 공통 로직을 찾는 것은 결코 쉬운 일이 아니다.

이를 관점 지향 프로그램(AOP, Aspect Object Programing)이라고 말하는데 우리가 흔히 알고있는 행위로서 상태에 변화를 주는 객체 지향그램(OOP, Object Programing)과는 달리 보는 OOP에서 독립적으로 분리하기 어려운 부가기능을 모듈화한다. 이 글에서는 validation이라는 라이브러리를 사용한다.

Test

테스트용 파일

Package
<com.cos.blogapp.test>
<UserControllerTest.java>

folder
<view/test>

jsp
<join.jsp>

Class
<UserControllertest.java>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
  <h1>test join page</h1>
</body>
</html>
package com.cos.blogapp.test;

import org.springframework.stereotype.Controller;

@Controller
public class UserControllertest {
  
  @GetMapping("/test/join")
  public @ResponseBody String testJoin() {
    return "test/join";
  }
  
}

 

@ResponseBody가 붙은 메서드가 데이터를 리턴하게 만들어요.
test
이렇게 코딩하면 무조건 데이터를 리턴하게 만들어버려요
@GetMapping("/test/login")
  public @ResponseBody String testLoin() {
    return "<script>alert('hello');</script>";
  }

 

자바스크립트를 이용해서 해결할 수 있어요.
//주소?name=홍길동 : 쿼리스트링 방식 주소 -> 스프링에서는 이방식 안씀 
///test/data/1
  @GetMapping("/test/data/{num}")
  public @ResponseBody String testData(@PathVariable int num) {  //@PathVariable이 {num}파싱해서 int num안에 넣어준다.
    return ""+num; //묵시적 형변환 -> 스프링으로 바뀌는 타입
  }

 

테이터와 파일 모두 리턴할 수 있게 코딩을 할 수 있어요
전체코드
package com.cos.blogapp.test;

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

@Controller
public class UserControllertest {
  
  @GetMapping("/test/join")
  public @ResponseBody String testJoin() {
    return "test/join";
  }
  
  @GetMapping("/test/login")
  public @ResponseBody String testLoin() {
    return "<script>alert('hello');</script>";
  }
  
  @GetMapping("/test/data/{num}")
  public @ResponseBody String testData(@PathVariable int num) { 
    
    if(num == 1) {
      StringBuilder sb = new StringBuilder ();
      sb.append("<script>");
      sb.append("location.href='/';");
      sb.append("</script>");
      
      return sb.toString(); 
    }else { 
      return "오류가 났습니다.";
    }
    
    
  }
}

 

1.util 패키지 안에 Alert 창으로 띄울 Script.java 파일을 만들어준다.
package com.cos.blogapp.util;

public class Script {

    //실패했을 때 메서드 
    public static String back(String msg) {
      StringBuilder sb = new StringBuilder ();//문자열 더할 때사용하는 클래스 객체
      sb.append("<script>");
      sb.append("alert(' "+msg+" ');"); //에러맵에 담긴 메시지(msg)를 alert창으로 띄운다.
      sb.append("history.back();"); //원래 페이지로 돌아간다.
      sb.append("</script>");
      
      return sb.toString();
    }
    
    //성공했을 때 메서드 - 이동만 하는 것
    public static String href(String path) {
      StringBuilder sb = new StringBuilder ();
      sb.append("<script>");
      sb.append("location.href='"+path+"';"); 
      sb.append("</script>");
      
      return sb.toString();
    }
    
    //성공했을 때 메서드 - alert 창을 띄우고 이동  
    public static String href(String path, String msg) {  //함수 이름 같게 한다(오버로딩:개발자에게 기억하기 쉽게). 매개변수 바꾼다.
      StringBuilder sb = new StringBuilder(); 
      sb.append("<script>");
      sb.append("alert(' "+msg+" ');"); 
      sb.append("location.href='"+path+"';"); 
      sb.append("</script>");
      
      return sb.toString();
    }
  
}

StringBuilder를 사용하는 이유

String는 레퍼런스 변수로 컴파일 시에 변수의 크기를 알 수 없다. 런타임 시에 변수의 크기를 알 수 있는데 이 때 String 변수를 띄워주는 heap 메모리의 용량보다 큰 데이터가 들어온 경우 heap 메모리의 사이즈를 늘리거나 분할할 필요가 있다.

String 변수에 새로운 데이터를 추가하면 새로운 공간을 만들어내기 때문에 원래 있던 공간은 쓰레기가 된다. 또한 String 변수는 데이터가 같은 변수는 새로운 공간을 따로 만들어내지 않고 같은 공간을 재활용하여 사용한다. 이는 메모리 공간을 절약할 수 있다는 장점이 있다.

이 프로젝트에서  StringBuilder를 사용하는 이유는 StringBuilder는 block이 걸리지 않아 클라이언트가 동시에 접속해도 대기하지 않고 고정된 데이터를 바로 보여줄 수 있게 하기위함이다. 이는  StringBuffer과의 유일한 차이점으로 데이터가 꼬이면 안되는 경우에는 StringBuffer를 사용하여 block을 걸어줄 수 있다.

2.회원가입과 로그인 그리고 글쓰기 메서드에서 Script 파일을 사용한 코드로 변경한다.
// 로그인
  @PostMapping("/login")
  public @ResponseBody String login(@Valid LoginReqDto dto, BindingResult bindingResult, Model model) {

    // 유효성 검사 실패
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      model.addAttribute("errorMap", errorMap);
      return Script.back(errorMap.toString());
    }

    // 유효성 검사 성공 
    String encPassword = SHA.encrypt(dto.getPassword());
    User principal = userRepository.mLogin(dto.getUsername(), encPassword);

    if (principal == null) {
      return Script.back("아이디 혹은 비밀번호를 잘못 입력하였습니다.");
    } else {
      session.setAttribute("principal", principal);
      return Script.href("/","로그인 성공");
    }
  }

  // 회원가입
  @PostMapping("/join")
  public @ResponseBody String join(@Valid JoinReqDto dto, BindingResult bindingResult, Model model) {

    // 유효성 검사 실패
    if (bindingResult.hasErrors()) {
      Map<String, String> errorMap = new HashMap<>();
      for (FieldError error : bindingResult.getFieldErrors()) {
        errorMap.put(error.getField(), error.getDefaultMessage());
      }
      model.addAttribute("errorMap", errorMap);
      return Script.back(errorMap.toString());
    }

    // 유효성 검사 성공
    // 비밀번호 해시로 변경
    String encPassword = SHA.encrypt(dto.getPassword());
    dto.setPassword(encPassword);
    User user = dto.toEntity();
    userRepository.save(user);
    return Script.href("/loginForm");

  }
@PostMapping("/board")
public @ResponseBody String save(@Valid BoardSaveReqDto dto, BindingResult bindingResult) {

  // 글쓰기 유효성 검사
  User principal = (User) session.getAttribute("principal");
  if (principal == null) {
    // 로그인이 되지 않은 상태에서 글쓰기한 경우
    return Script.href("/loginForm", "잘못된 접근입니다.");
  }
  
  //유효성 검사 실패 
  if (bindingResult.hasErrors()) {
    Map<String, String> errorMap = new HashMap<>();
    for (FieldError error : bindingResult.getFieldErrors()) {
      errorMap.put(error.getField(), error.getDefaultMessage());
    }
    return Script.back(errorMap.toString());
  }

  //유효성 검사 성공
  boardRepository.save(dto.toEntity(principal));
  return Script.href("/","글쓰기 성공");
}
Alert 창을 사용한 유효성 검사 결과 화면
<회원가입 실패한 경우>
alert

 

<로그인 실패한 경우>
alert
<로그인 성공한 경우>
alert
<글쓰기 실패한 경우>
alert
<글쓰기 성공한 경우>
alert
<로그인 하지 않고 글쓰기 한 경우>
alert
최신글