일상 및 회고/현대자동차 소프티어

3주차 미션 - 동적인 HTML과 쿠키,세션을 이용한 로그인

규턴이 2023. 1. 22. 14:54

3주차 미션은 다음과 같았다

 

1. GET으로 회원가입 -> POST로 변경

 

2. 동적인 HTML요구(로그인, 비로그인시 메인화면 페이지가 동적으로 변할 수 있어야함

  • 로그인시: 사용자의 이름이 보여야하고, 로그인 회원가입 버튼이 삭제
  • 비로그인시: 사용자의 이름이 보이지 않고, 로그인 회원가입 버튼이 보여야함

 

3. 쿠키 세션을 이용한 로그인/회원가입 기능 요구

 

 

1. GET으로 회원가입 -> POST로 변경

 

나는 이전에 중간 피드백시간에서 한 팀원이 리플렉션을 사용하여 컨트롤러를 선택하는 로직을 보았다.

해당 로직은 인상깊었고, 실제로 스프링도 Reflection을 사용하여 로직이 구현되는것으로 알 고 있다.

그렇기에 이번에 Reflection을 공부해볼겸 실제 프로젝트에도 적용해보았다!

 

public static void findController(HttpRequest httpRequest, DataOutputStream dataOutputStream) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, FailLoggedException, InvocationTargetException, NoSuchMethodException, NotLoggedException {
        String packageName = "controller";
        ClassPath classpath = ClassPath.from(Thread.currentThread().getContextClassLoader());
        ImmutableSet<ClassPath.ClassInfo> havingControllerInfoClasses = classpath.getTopLevelClassesRecursive(packageName);
        for (ClassPath.ClassInfo classInfo : havingControllerInfoClasses) {
            Class<Controller> clazz = (Class<Controller>) Class.forName(classInfo.getName());
            if (clazz.isAnnotationPresent(ControllerInfo.class)) {
                handleControllerInfoAnnotation(clazz.newInstance(), httpRequest, dataOutputStream);
            }
        }

    }

    public static void handleControllerInfoAnnotation(Controller controller, HttpRequest httpRequest, DataOutputStream dataOutputStream) throws IllegalAccessException, InvocationTargetException, NotLoggedException {
        Method[] methods = controller.getClass().getMethods();

        Optional<Method> method = Arrays.stream(methods)
                .filter(m -> m.isAnnotationPresent(ControllerMethodInfo.class))
                .filter(m -> isMatchControllerMethodWithRequest(m.getAnnotation(ControllerMethodInfo.class), httpRequest))
                .findAny();

        if (method.isEmpty()) {
            return;
        }

        if (method.get().isAnnotationPresent(Auth.class) && !sessionService.cookieValidInSession(httpRequest.getRequestHeader())) {
            throw new NotLoggedException(HttpsErrorMessage.NOT_LOGGED);
        }
        method.get().invoke(controller, dataOutputStream, httpRequest);
    }

 

  • FindController함수는 @ControllerInfo라는 어노테이션이 달린 클래스를 팢는다
  • handleControllerInfoAnnoatation 은 @ControllerMethodInfo 어노테이션이 달린 메서드를 찾아 해당 메서드의 멤버변수인 path, http method 가 request와 일치하면 해당 메서드(컨트롤러)를 호출한다.

 

@ControllerInfo(path = "/user/create", u = UrlType.NOTHING, method = HttpMethod.POST)
    public HttpResponse UserQueryString(DataOutputStream dataOutputStream, HttpRequest httpRequest) throws IOException {
        requestReader = new RequestPostReader();

        HashMap<String, String> userMap = requestReader.readData(httpRequest);
        User user = (User) userService.createModel(userMap);
        userDatabase.addData(user);

        HttpResponse httpResponse = new HttpResponse(new Data(dataOutputStream), FileType.HTML, HttpStatus.RE_DIRECT);

        logger.debug("저장된 user:{}", userDatabase.findAll());

        return httpResponse;
    }

 

확실히 어노테이션과 Reflection을 사용하니 method= HttpMethod.Get을 Post로 바꾸고 Request데이터를 읽는방식만 바꾸면 되었기에 좀더 수월하였다.

 

 

2. 동적인 HTML요구

 

package view;

public class HomeRender {
    private static HomeRender homeRender;

    private HomeRender() {}

    public static HomeRender getInstance() {
        if (homeRender == null) {
            homeRender = new HomeRender();
        }
        return homeRender;
    }


    public byte[] addSignInAndUpTag(byte[] homeData) {
        String homeStr = new String(homeData);
        homeStr = homeStr.replace("<!--                <li><a href=\"user/login.html\" role=\"button\">로그인</a></li>-->",
                "                <li><a href=\"user/login.html\" role=\"button\">로그인</a></li>");
        homeStr = homeStr.replace("<!--                <li><a href=\"user/form.html\" role=\"button\">회원가입</a></li>-->",
                "                <li><a href=\"user/form.html\" role=\"button\">회원가입</a></li>");
        return homeStr.getBytes();
    }

    public byte[] addUserName(byte[] homeData,String userName) {
        String homeStr = new String(homeData);
        homeStr = homeStr.replace("<!--                <li><a href=\"#\">이름</i></a></li>-->",
                "                <li><a href=\"#\">"+userName+"</i></a></li>");
        return homeStr.getBytes();
    }
}
  • 싱글톤으로 view를 동적으로 변경하게 해주는 클래스로 생성
  • addSignInAndUpTag, addUserName모두 이전 index.html을 태그 중 필요한곳에 replace를 하는 기능이다.

즉! 주석으로 처리한 부분을 String의 replace를 사용하여 기능을 구현해 보았다!

로그인

 

비로그인

 

3. 쿠키 세션을 이용한 로그인/회원가입 기능 요구

 

쿠키의 동작 흐름

난 쿠키가 누구에게 주도권이 있는지 몰랐다.

하지만 이번에 쿠키를 다루면서 가장 크게 안 사실!

-> 쿠키는 서버가 지정해줄수 있다!(Set-Cookie를 통해) -> 즉 쿠키의 주도권은 서버에게 있다

 

 

쿠키

  • 요청의 주체는 서버이다! (setCookie)
  • 서버가 SetCookie를 저장하면, 다음 http Request시 매번 cookie를 채워 보낸다.

세션

  • 서버에서 사용자 하나마다 저장되는 단위
  • 세션 ID를 긴 랜덤값 사용 이유 → BruteForce 공격이 가능
  • 세션 DB는 주로 redis를 사용한다
    • 일회성값일 수 있는 session을 RDB등과 같은 DB에 저장하면 매우느림
    • 실무에서는 redis를 cluster로 구성하여 크게 사용한다고 함
  • 세션 클러스터링을 통한 SCALE UP

 

세션과 쿠키를 사용할때 가장 중요하게 생각하는 흐름!

  • 클라이언트와 서버 사이는 Cookie를 통해 Session Id만 주고 받는다.
  • 서버는 클라이언트가 전송한 Session Id를 활용해 서버에 저장된 값을 꺼내와 사용한다.

 

(코드 보기싫으면 그냥 넘겨두됨)

public class Cookie {
    private static final String COOKIE = "Cookie";
    private String key;
    private String value;

    public Cookie(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }

    public static Optional<Cookie> extractCookie(RequestHeader requestHeader) {
        String cookieSet = requestHeader.getHeaderContents().get(COOKIE);

        if (cookieSet == null) {
            return Optional.empty();
        }

        String[] splitCookie = cookieSet.split("=");
        return Optional.of(new Cookie(splitCookie[0], splitCookie[1]));
    }
}

 

public class Session {

    public static final String SESSION_ID = "sid";

    private String uuid;

    private final User user;

    public Session( User user) {
        this.uuid = makeSessionId();
        this.user = user;
    }

    public String getUuid() {
        return uuid;
    }

    private String makeSessionId() {
        return UUID.randomUUID().toString();
    }


    public User getUser() {
        return user;
    }
}

 

 

간단하게 정리하면 다음과 같다.

1. 로그인/회원가입시 Session을 하나 생성 (uuid, User)를 가짐 -> 이후 DB에 저장

2. HttpResponse에 Set-Cookie : sid="uuid 값" 을 클라이언트에 보내줌

3. 클라이언트는 항상 cookie값을 서버에게 보내줌

4. 만약 로그인(인증)이 필요한 상황이라면 클라이언트로 받은 uuid 를 가지고 User객체를 찾아 User객체가 있다면 유효한 로그인으로 판단

 

 

 

 

 

 

 

해당 미션을 하면서 느꼈던것은

1. 확실히 공부를 할때는 내가 아는거 30%, 모르는게 70%정도의 비율은 되어야 공부가 되는거 같다. (Reflection을 적용할때 내가 아는 부분 30퍼(이론)밖에 없었고 실제 적용해보니 더 이해가 잘된것을 확인할 수 있었다.

 

2. 쿠키 세션도 이론부분만 알고있었고 실제사용해보진 않았다. 로우레벨로부터 접근하니 원리를 확실하게 알 수 있었다.

 

3. 동적인 HTML을 구현하는 더 좋은 방법이 있을거 같다. 실제로 '타임리프'와 같은 템플릿엔진은 모델을 넘겨주고 동적인 HTML을 구현하는것으로 알 고 있다. 나도 단지 replace를 하는 방법이 아닌 model을 넘겨주는 방식으로 구현해볼 수 있지 않을까??