🔗 참고자료

  • 블로그 <어제보다 더 나은 개발자> spring-boot init sql (schema.sql) 이란? => 링크

 

✍ 공부하게 된 계기

JPA를 사용했을 때는 ddl-auto로 model에 설계한 대로 자동으로 처리를 해줬다.

그런데 마이바티스를 사용하면서 schema.sql이라는 것으로 초기에 DB에 쿼리문을 날리도록 한다는걸 알게되었다.

마이바티스를 처음 사용할 때 data.sql 파일에다가 ddl을 작성하는 실수도 했었다.

그래서 schema.sql과 data.sql에 대해 좀 더 찾아보고 정리하게 되었다.

 

 

 

 

schema.sql

  • 어플리케이션을 로딩 시 쿼리문을 실행하고 싶을 경우 초기에 db에 쿼리문을 날릴 때 유용하다.
  • 스프링 이용할 때 schema.sql 파일을 사용해서 초기 스키마를 생성할 수 있다.
  • schema.sql은 ddl을 작성한다
  • 스키마 : 데이터베이스를 구성하는 데이터 개체(Entity), 속성(Attribute), 관계(Relationship) 및 데이터 조작 시 데이터 값들이 갖는 제약 조건등에 관해 전반적으로 정의한다.

 

 

 

data.sql

  •  스프링 부트 초기 DB 데이터를 삽입하는 쿼리문을 작성해놓는 곳이다.
  • data.sql은 dml을 작성한다. 

 

 

추가내용

  • Spring boot 2.4 이하 버전의 경우 **src/resource/** 하위에 schema.sql이나 data.sql파일이 있는 경우 자동 실행
    => 실행되지 않을 경우 spring.datasource.initialization-mode=always 를 세팅한다.
  • Spring boot 2.5 이상 버전의 경우 자동으로 실행되지 않아 spring.sql.init.mode=always 프로퍼티 값을 입력
  • JPA와 함께 사용하는 경우 Hibernate의 ddl-auto 기능과 함께 schema.sql을 실행하면 schema.sql이 먼저 실행되고
    Hibernate의 기능이 수행된다.
    => 이 순서를 바꾸고 싶으면 spring.jpa.defer-datasource-initialization=true 옵션을 입력한다.
    => schema.sql로만 설정하고 싶으면 spring.jpa.hibernate.ddl-auto=none 옵션을 입력한다.
반응형

🔗 참고자료

  • 블로그 <Taes-k DevLog> Tomcat, Spring MVC의 동작 과정 => 링크
  • 블로그 <heejeong Kwon> [SpringMVC] Spring MVC Framework란 => 링크
  • 블로그 <진열사랑> [tomcat] 동작원리 => 링크
  • 블로그 <일단은 내 이야기> [Java]Apache? Tomcat?? 둘이 무슨 차이지? => 링크

 

✍ 공부하게 된 계기

Dispatcher Servlet을 직접 구현해보면서 Tomcat과 Spring MVC의 동작 과정을 알고 있어야 구현하는데 도움이 된다고 생각했습니다. 기본적으로 작동하는 원리를 모르고 구현을 하는건 제 기준에서는 거의 불가능하다고 생각했습니다.

설령 구현을 한다고 해도 나 자신에게 도움되는게 거의 없을 것 같다고도 느꼈습니다.

Spring Boot를 사용하면서 내장 Tomcat이 있어서 개발자가 직접 실행을 시키지 않아도 된다고만 알고있습니다.

단순히 위의 개념만 가지고는 너무 부족한 것 같아서 이렇게 공부하게 되었습니다.

 

 

 

❓ 톰캣(Tomcat)이란

  • 일반적으로 톰캣(Tomcat)은 'WAS(Web Application Server)' 의 대표적인 미들웨어 서비스로 알려져 있다.
    하지만 톰캣은 일반적으로 아파치 톰캣(Apache Tomcat)이라 불리며 회사명이자 웹서버의 대표적인 미들웨어인
    아파치(Apache)의 기능 일부분을 가져와 함께 사용되면서 웹서버(Web Server)의 기능과 웹 애플리케이션 서버(Web Application Server) 모두를 포함하고 있다고 생각해도 무방하다.

 

Tomcat의 동작원리

  1. Http Request를 Servlet Container에 전송
  2. Servlet Container는 HttpServletRequest, HttpServletResponse 두 객체를 생성
  3. 사용자가 요청한 URL을 분석해 어느 서블릿에 대한 요청인지 탐색
  4. 만약 해당 서블릿이 한번도 실행된 적 없거나, 현재 메모리에 생성된 인스턴스가 없다면
    인스턴스를 생성하고 init() 메소드를 실행하여 초기화한 뒤 스레드를 하나 생성한다.
    이미 인스턴스가 존재할 경우에는 스레드만 하나 생성한다.
    (각 서블릿 인스턴스는 서블릿 컨테이너 당 하나만 존재하기 때문이다.)
  5. 컨테이너는 서블릿 service() 메소드를 호출하며, POST, GET 여부에 따라 doGet()또는 doPost()를 호출
  6. 실행된 메소드는 동적인 페이즈를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
  7. 응답이 완료되면 HttpServletRequest, HttpServletResponse 두 객체를 소멸한다.

 

아파치(Apache)와 톰캣(Tomcat)의 차이

  • 아파치 서버란 클라이언트에서 요청하는 HTTP요청을 처리하는 웹서버를 의미한다.
    이는 정적타입(HTML, CSS, 이미지 등)의 데이터만을 처리한다.
  • 톰캣 WAS(Web Application Server)는 컨테이너 웹 컨테이너 서블릿 컨테이너로도 불린다.
    • 컨테이너 : 동적인 테이터들을 가공하여 정적인 파일로 만들어주는 모듈
    • 서블릿 : 클라이언트의 요청을 받고 요청을 처리하여 결과를 클라이언트에게 제공하는 자바 인터페이스.
      java.servlet.package에 정의된 인터페이스로서 서블릿의 라이프 사이클을 위한 세 가지 필수적인 메소드를 정의
      - init(), service(), destory()
    • 서블릿 컨테이너 : 서블릿들을 모아 관리하고 새로운 요청이 들어올 때마다 새로운 스레드를 생성한다.
      작업이 끝난 서블릿 스레드는 자동 제거된다.
    • WAS : DB처리, 로직 처리를 요구하는 동적타입을 제공하는 소프트웨어 프레임워크를 의미한다.
      기본적으로 사용되는 기능 3가지는 아래와 같다.
      - 프로그램 실행 환경과 데이터베이스 접속 기능을 제공한다.
      - 여러 개의 트랜잭션을 관리한다.
      - 업무를 처리하는 비즈니스 로직을 수행한다.

정리하면

아파치 서버는 정적인 파일을 처리해주는 웹서버(80포트)이고,

톰캣 DB처리와 같은 동적인 기능을 가공하여 HTML파일로 만들어 클라이언트에게 제공(8080포트)한다.

 

하지만 아파치 톰캣으로 불리는 경우가 있는 그 이유는 

기본적으로 아파치와 톰캣의 기능은 분리되어 있지만 톰캣 안에 있는 컨테이너를 통해 일부 아파치의 기능을 발휘하기 때문이다. 보통 아파치 톰캣으로 합쳐서 부르곤 한다.

 

 

❓ Spring MVC란

  • Spring MVC는 Spring에서 제공하는 웹 모듈로, Model, View, Controller 세가지 구성요소를 사용해
    사용자의 다양한 HTTP Request를 처리한다.
  • 단순한 텍스트 형식의 응답부터 REST 형식의 응답은 물론 View를 표시하는 html을 return하는 응답까지 다양한 응답을 할 수 있도록 하는 프레임워크이다.

 

Spring Framework가 제공하는 Class와 동작과정

  • DispatcherServlet
    Spring Framework가 제공하는 Servlet 클래스로 사용자의 요청을 최초로 받는 Front Controller이다.
    Dispatcher가 받은 요청은 HandlerMapping으로 넘어간다.
  • HandlerMapping
    사용자의 요청을 처리할 Controller를 찾는다.(Controller URL Mapping)
    요청 url에 해당하는  Controller 정보를 저장하는 table을 가진다.
    즉, 클래스에 @RequestMapping("/url") 어노테이션을 명시하면 해당 URL에 대한 요청이 들어왔을 때
    table에 저장된 정보에 따라 해당 클래스 또는 메서드에 Mapping한다.
  • ViewResolver
    Controller가 반환한 View Name(the logical names)에 prefix, suffix를 적용하여
    View Object(the physical view files)를 반환한다.
    예를 들어 view name:home, prefix: /WEB-INF/views/, suffix: .jsp는
    "/.WEB-INF/views/home.jsp"라는 위치의 View(JSP)에 Controller에게 받은 Model을 전달한다.
    이 후에 해당 View에서 이 Model data를 이용하여 적절한 페이지를 만들어 사용자에게 보여준다.

 

 

반응형

 

 

🔗 참고자료

 

✍ 공부하게 된 계기

면접을 준비하면서 자주 나왔던 내용입니다.

질문에 대한 답을 그냥 이론만 거의 외워서 대답은 했었습니다.

정확하게 어떻게 DispatcherSevlet이 구현되어 있는지 알지 못했습니다.

단순히 이론을 외우면 계속 까먹고 제대로 이해하기가 힘들다고 생각했습니다.

그래서 이렇게 직접 구현을 해보면서 좀 더 깊게 공부하기로 했습니다.

 

 

 

깃허브 링크

  • 구현중인 깃허브 레포지토리 => 링크
  • 우아한테크코스에서 구현한 레포지토리 => 링크

전부 혼자서 구현하기에는 힘들기 때문에 다양한 자료들을 바탕으로 만들예정입니다.

 

 

1. DispatcherSevlet이란?

  • Dispatch는 "보내다" 라는 뜻을 가지고 있다.
  • HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러이다.
    * 프론트 컨트롤러(Front Contoller)
  • 클라이언틀로부터 어떠한 요청이 오면 톰캣(Tomcat)과 같은 서블릿 컨테이너가 요청을 받게 된다.
    그리고 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게 된다.
    그 후 공통적인 작업을 먼저 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임한다.
  • 프론트 컨트롤러는 MVC 구조에서 함께 사용되는 디자인 패턴이다.

 

 

 

2. DispatcherServlet의 동작 과정

  1. DispatcherServlet으로 클라이언트의 웹 요청(HttpServletRequest)가 들어온다.
  2. 웹 요청을 LocaleResolver, ThemeResolver, MultipartResolver 인터페이스 구현체에서 분석한다.
  3. 웹 요청을 HandlerMapping에 위임하여 해당 요청을 처리할 Handler(Controller)를 탐색한다.
  4. 찾은 Handler를 실행할 수 있는 HandlerAdapter를 탐색한다.
  5. ④, ⑤ 찾은 HandlerAdapter를 사용해서 Handler의 메소드를 싱행한다. 이때, Handler의 반환값은 ModelView이다.
  6. ⑥ View 이름을 ViewResolver에게 전달하고, ViewResolder는 해당하는 View 객체를 반환한다.
  7. ⑦ DispatcherServlet은 View에게 Model을 전달하고 화면 표시를 요청한다. 이때, Model이 null이면 View를 그대로 사용한다. 반면, 값이 있으면 View에 Model 데이터를 렌더링한다.
  8. ⑧ 최종적으로 DispatcherServlet은 View 결과(HttpServletResponse)를 클라이언트에게 반환한다.

=> @Controller를 기준으로 작성한 것이고 @RestContoller의 경우 ⑥, ⑦ 과정이 생략된다. 즉 ViewResolver를 타지 않고 반환값에 알맞는 MessageConverter를 찾아 응답 본문을 작성한다.

 

 

 

3. 구현해야할 것들

  • DispatcherServlet 클래스
  • Controller 인터페이스
  • HandlerMapping 클래스
  • ViewResolver 클래스

 

 

4. Dependencies(gradle)

dependencies {
	// jakarta servlet api 라이브러리
    implementation 'jakarta.servlet:jakarta.servlet-api:5.0.0'

	// 아파치 톰캣
    implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.0-M16'
    implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:10.1.0-M16'
}

 

 

 

 

5. 소스코드

 

 

DispatcherServlet 클래스

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class DispatcherServlet  extends HttpServlet {
    private HandlerMapping handlerMapping;
    private ViewResolver viewResolver;

    @Override
    public void init() throws ServletException {
        handlerMapping = new HandlerMapping();
        viewResolver = new ViewResolver();
        viewResolver.setPrefix("./");
        viewResolver.setSuffix(".jsp");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        service(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        service(req, resp);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String uri = req.getRequestURI();
        String path = uri.substring(uri.lastIndexOf("/"));

        Controller controller = handlerMapping.getController(path);

        String viewName = controller.handleRequest(req, resp);

        String view = null;
        if (!viewName.contains(".do")) {
            view = viewResolver.getView(viewName);
        } else {
            view = viewName;
        }
    }
}
  • 코드의 동작 과정
    1. 사용자의 요청을 최전선에서 DispatherServlet이 받는다.
    2. URI를 Parsing해서 Path를 HandlerMapping에게 넘긴다.
    3. HandlerMapping은 사용자 요청을 처리할 수 있는 올바른 Controller를 리턴한다.
    4. Controller는 내부의 HandlerRequest를 통해서 요청을 처리한다.
    5. 요청을 처리한 뒤 이동할 화면을 ViewResolver를 통해 JSP(혹은 View)파일의 이름과 경로를 리턴받는다.
    6. 사용자에게 올바른 뷰를 보여준다.

 

Controller 인터페이스

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public interface Controller {
    String execute(HttpServletRequest req, HttpServletResponse res) throws Exception;
}
  • 클라이언트의 요청을 받은 DispatcherServlet은 HanlderMapping을 통해 Controller 객체를 검색하여 실행한다.
    이때 어떤 Controller 객체가 검색되더라도 같은 코드로 실행하려면, 모든 Controller의 최상위 인터페이스가 필요하다.

 

 

 

6. 용어 정리

 

❓ Handler란

  • Spring MVC에서는 자동차의 핸들과 마찬가지로 클라이언트의 요청을 처리하는 처리자를 Handler라고 한다.
  • Spring MVC에서 Handler는 Controller 클래스를 의미한다.
    Controller 클래스에 있는 '@GetMapping, @PostMapping' 같은 어노테이션이 붙어 있는 메서드들을
    핸들러 메소드라고 한다

.

❓ HandlerMapping이란

  • 클라이언트의 요청과 이 요청을 처리하는 Handler를 매핑해주는 역할을 하는 것이다.
  • @GetMapping("/login") 처럼 HTTP request Method(GET, POST등)와 Mapping URL을 기준으로 해당 Handler와 
    매핑이 되는데 Spring 에서는 여러가지 유형의 HandlerMapping 클래스를 제공하고 있다.

 

 HandlerAdapter란

  • 컨트롤러(핸들러)가 처리한 결과값을 받아서 ModelAndView 형태로 바꿔 DispatcherServlet에 보내준다.
    이런 기능 덕분에 어떤 형태의 객체로 결과값을 받던지간에 상관없이 웹요청을 처리할 수 있게 된다.
  • ex) 컨트롤러에서 String 값을 리턴해주어도 그 값을 ModelAndView로 변환해서 DispatcherServlet으로 보내준다.

 

 

❓ ViewResolver란

  • Resolver 는 무언가를 해석하고, 해결해주다라는 뜻이 있다.
  • ViewResolver는 DispatcherServlet에서 '이런 이름을 가진 View를 줘'라고 요청하면 DispatcherServlet에서 전달한
    View 이름을 해석한 뒤 적절한 View 객체를 리턴해주는 역할을 합니다.
반응형

 

 

🔗 참고자료

  • 유튜브 Amigoscode 채널 => 링크
  • [10분 테코톡] 작은곰의 Spring Security => 링크
  • <Spring Security 란?> 블로그 망나니개발자 => 링크
  • <JWT(Json Web Token)란?> 블로그 망나니개발자 => 링크
  • <JWT란 무엇인가?> 벨로그 hahan => 링크
  • JJWT 라이브러리 관련 공식문서 정리 및 변역된 블로그 => 링크
  • 블로그 <삽질중인 개발자> => 링크

 

✍ 공부하게 된 계기

프로젝트를 진행하면서 스프링 시큐리티는 대부분 이미 구현되어 있는 것을 가져다 사용하는 방식을 했습니다.

그래서 인증과 인가가 어떻게 이뤄지고, 어떤 필터를 타서 인증이 진행되는지 모르는 결과를 초래하게 되었습니다.

구현을 했는데 내부적으로 어떻게 돌아가는지 대략적으로도 모르고, 결국에 구현 후 나에게 남는 게 많이 없을 것 같다고 생각돼서 이렇게 조금이라도 깊게 공부하게 되었습니다.

그리고 공부를 하면서 나오는 다양한 개념(CSRF, JWT, Basic Auth 등등)들이 있어서, 개발자로서 간단한 보안에 대한 시각도 넓혀주고 도움이 될 것이라고 생각해서 이렇게 글을 작성하게 됐습니다.

 

 

 

 

 

1. 인증(Authentication)과 인가(Authorization)

  • 인증(Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차
  • 인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차

스프링 시큐리티는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며,
인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인을 하게 됩니다.

스프링 시큐리티에서는 이러한 인증과 인가를 위해

Principal을 아이디로,

Credential을 비밀번호로 사용하는

Credential 기반의 인증 방식을 사용한다.

 

  • Principal(접근 주체) : 보호받는 Resource에 접근하는 대상
  • Credential(비밀번호) : Resource에 접근하는 대상의 비밀번호

 

 

 

2. 스프링 시큐리티(Spring Security)란?

  • 스프링 기반의 애플리케이션 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.
  • '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
    • Filter는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만,
      Interceptor는 Dispatcher와 Controller 사이에 위치한다는 점에서 적용 시기의 차이가 있다.
    • 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이
      보안관련 로직을 작성하지 않아도 된다는 장점이 있다.

 

 

 

3. JWT(Json Web Token)란?

  • Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
  • 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다.

 

 

4. JWT의 구성요소

JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 파트로 나눠져 있다.

 

  • 헤더 (Header)
    어떠한 알고리즘으로 암호화 할 것인지, 어떠한 토큰을 사용할 것 인지에 대한 정보가 담겨있다.

  • 정보 (Payload)
    전달할려는 정보(사용자 id나 다른 데이터들, 이것들을 크렘이라고 부른다)가 들어있다.
    payload에 있는 내용은 수정이 가능하여 더 많은 정보를 추가할 수 있다.
    그러나 노출과 수정이 가능한 지점이기 때문에 인증이 필요한 최소한의 정보만을 담아야 한다.
    (아이디, 비밀번호 등 개인정보가 아닌 이 토큰을 가졌을 때 권한의 범위나 토큰의 발급일과 만료일자 등)
  • 서명 (Signature)
    가장 중요한 부븐으로 헤더와 정보를 합친 후 발급해준 서버가 지정한 secret key로 암호화 시켜 토큰을 변조하기
    어렵게 만들어준다.
    한가지 예를 들어보자면 토큰이 발급된 후 누군가가 payload의 정보를 수정하면 payload에는 다른 누군가가
    조작된 정보가 들어가 있지만 signature에는 수정되기 전의 payload 내용을 기반으로 이미 암호화 되어있는
    결과가 저장되어 있기 때문에 조작되어 있는 payload와는 다른 결과값이 나오게 된다.
    이러한 방식으로 비교하면 서버는 토큰이 조작되었는지 아닌지를 쉽게 알 수 있고,
    다른 누군가는 조작된 토큰을 악용하기가 어려워진다.

 

 

 

 

5. 일반 토큰 기반과 클레임 토큰 기반 차이

  • 일반 토큰 기반 인증
    • 검증할 때 필요한 관련 정보들을 서버에 저장해두고 있다.
      => 인증 시 DB에 접근하는 과정이 추가된다.
    • session 방식 또한 저장소에 저장해두었던 session ID를 찾아와 검증하는 절차를 가져야 한다.
  • 클레임 토큰 기반
    • 클레임 토큰 기반 중 하나인 JWT는 사용자 인증에 필요한 정보를 토큰 자체에 담고 있다.
      => 별도의 인증 저장소가 필요 없다.
    • 분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는다.
      => 일반 토큰 기반 인증에 비해 편리한 인증 절차

 

 

 

6. JWT 인증 절차

  1. [클라이언트] 자격 증명에 필요한 정보 전송
    - 로그인 시 유저의 아이디, 비밀번호와 같은 자격 증명에 필요한 정보 전송
  2. [서버] 전송받은 자격 증명 정보를 확인(검증)
  3. [서버] 검증 완료된 정보를 기반으로 토큰 생성 및 서명
  4. [서버] 생성된 토큰을 클라이언트에게 전송
  5. [클라이언트] 전송받은 토큰을 기반으로 리퀘스트를 보낼 때 마다 토큰을 전송

 

 

 

 

7. JWT 라이브러리

https://github.com/jwtk/jjwt

 

GitHub - jwtk/jjwt: Java JWT: JSON Web Token for Java and Android

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

 

  • JJWT는 JVM과 안드로이드에서 쉽게 JWT를 생성하고 확인(검증) 할 수게 하는 라이브러리입니다.
  • JJWT는  Apache 2.0 라이선스에 따라 JWT, JWS, JWE, JWK, JWA RFC 명세 및 오픈소스를
    독점적으로 기반하는 순수 Java 구현이다.

 

Dependencies

 

<Maven>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

 

<Gradle>

dependencies {
    compile 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtime 'io.jsonwebtoken:jjwt-impl:0.11.5',
    // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
    //'org.bouncycastle:bcprov-jdk15on:1.70',
    'io.jsonwebtoken:jjwt-jackson:0.11.5' // or 'io.jsonwebtoken:jjwt-gson:0.11.5' for gson
}

 

String secretKey = "securesecuresecuresecuresecuresecuresecuresecuresecure";

String token = Jwts.builder()
        .setSubject(authResult.getName())
        .claim("authorities", authResult.getAuthorities())
        .setIssuedAt(new Date())
        .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(1)))
        .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
        .compact();
  • 위와 같이 간단하게 JWT 토큰을 만들 수 있다.
  • 대부분의 Header와 Payload에 대한 정보를 이미 JJWT 라이브러리에서 제공한다.
  • JWT 토큰의 Header에 들어가는 알고리즘에 대한 정보는 어떤 알고리즘을 사용하느냐에 따라서 JJWT가 알아서 헤더에 추가해준다.

 

 

8. Spring Security 모듈

 

SecurityContextHolder

  • 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.
  • SecurityContext를 SecurityContextHolder가 관리한다.
  • 기본적으로 ThreadLocal을 사용한다.
    - ThreadLocal : 한 쓰레드 내에서 사용하는 공용 저장소
    - ThreadLocal을 사용해서 'Authentication'을  한 쓰레드 내에서 공유가 가능하다.
    - 쓰레드가 달라지면 제대로 된 인증 정보를 가져올 수 없다.
  • 즉 SecurityContextHolder란 Authentication을 담고 있는 Holder라고 정의할 수 있다.
  • 'Authentication' 자체는 인증된 정보이기에 'SecurityContextHolder'가 가지고 있는 값을 통해 인증이 되었는지
    아닌지 확인 할 수 있다. => Authentication.isAuthenticated();

 

SecurityContext

  • Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.

 

Authentication

  • 현재 접근하는 주체의 정보와 권한을 담는 인터페이스입니다.
  • SecurityContext에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고,
    SecurityContext를 통해 Authentication에 접근할 수 있습니다.

 

 

 

 

 

 

9. 구현 소스코드

JwtUserNamePasswordAuthenticationFilter 코드

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Date;

@RequiredArgsConstructor
public class JwtUserNamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response
    ) throws AuthenticationException {

        try {
            UsernamePasswordAuthenticationRequest authenticationRequest = new ObjectMapper()
                    .readValue(request.getInputStream(), UsernamePasswordAuthenticationRequest.class);

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    authenticationRequest.getUsername(),
                    authenticationRequest.getPassword()
            );
            return authenticationManager.authenticate(authentication);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    // attemptAuthentication이 정상적으로 작동되면 아래 메서드가 작동되게 된다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult
    ) {
        String token = Jwts.builder()
                .setSubject(authResult.getName())
                .claim("authorities", authResult.getAuthorities())
                .setIssuedAt(new Date())
                .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusWeeks(1)))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .compact();

        response.addHeader("Authorization",  "Bearer" + token);
    }
}

 

  • request안에서 username, password 파라미터를 가져와서 UsernamePasswordAuthenticationToken을 생성 후
    AuthenticationManager을 구현한 객체에 인증을 위임한다.
    => ObjectMapper()를 사용해서 JSON 컨텐츠를 Java 객체로 deserialization 한다.
    * ObjectMapper는 Java 객체를 deserialization 하거나 Java 객체를 JSON으로 serialization 할 때 사용하는
    Jasckson 라이브러리의 클래스이다. ObjectMapper는 생성 비용이 비싸기 때문에 bean/static으로 처리하는 것이 좋다

 

UsernamePasswordAuthenticationFilter 란?

  • 요청정보를 받아서 정보 추출을 하여 인증객체를 생성한다.
  • Form based Authentication 방식으로 인증을 진행할 때 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터이다.
  • 유저가 로그인 창에서 Login을 시도할 때 보내지는 요청에서 아이디(username)와 패스워드(password) 데이터를
    가져온 후 인증을 위한 토큰을 생성 후 인증을 다른 쪽에 위임하는 역할을 하는 필터입니다.

 

UsernamePasswordAuthenticationFilter 로직을 순서

 

 

UsernamePasswordAuthenticationToken란?

  • Authentication을 구현한 AbstractAuthenticationToken의 하위 클래스로 usernamePrincipal의 역할을 하고,
    passwordCredential의 역할을 합니다.
  • 첫 번째 생성자는 인증 전의 객체를 생성하고, 두 번째 생성자는 인증이 완료된 객체를 생성해줍니다.

 

AuthenticationManager란?

  • 인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리됩니다.
  • 인증이 성공하면 isAuthentication-true인 객체를 생성하여 SecurityContext에 저장합니다.
  • 인증 상태를 유지하기 위해 세션에 보관하며, 실패할 경우에는 AuthenticationException을 발생시킵니다.

 

 

JwtTokenVerifier 코드

import com.google.common.base.Strings;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class JwtTokenVerifier extends OncePerRequestFilter {

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain
    ) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (Strings.isNullOrEmpty(authorizationHeader) || authorizationHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authorizationHeader.replace("Bearer ", "");
        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
                    .build()
                    .parseClaimsJws(token);

            Claims body = claimsJws.getBody();

            String username = body.getSubject();

            var authorities = (List<Map<String, String>>) body.get("authorities");

            Set<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
                    .map(m -> new SimpleGrantedAuthority(m.get("authority")))
                    .collect(Collectors.toSet());

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    username,
                    null,
                    simpleGrantedAuthorities
            );

            SecurityContextHolder.getContext().setAuthentication(authentication);

        } catch (JwtException e) {
            throw new IllegalStateException(String.format("Token %s cannot be trust", token) );
        }
    }

}

 

  • 클라이언트에게서 넘어온 JWT토큰을 한번만 인증하면 되기 때문에 OncePerRequestFilter를 사용한다.
  • jjwt 라이브러리를 사용해서 토큰을 쉽게 파싱합니다.
    => 파싱한 토큰 내의 데이터를 authentication 객체에 저장 후 SecurityContextHolder에 set 합니다.

 

OncePerRequestFilter란?

  • 모든 서블릿에 일관된 요청을 처리하기 위해 만들어진 필터
  • 한 요청당 반드시 한 번만 실행된다.

 

 

위 소스코드에서 FilterChain이란?

  • 톰캣은 등록된 필터들의 클래스를 모두 객체화해서 내부 저장합니다.
    필터가 초기화될 때 필요한 부분들 또한 마찬가지입니다.
    이 객체들은 설정된 대로 순서를 가지게 되는데,
    이 순서 정보를 가진 "FilterChain" 인터페이스 객체르 메소드의 파라미터로 넘겨줍니다.
  • 모든 필터 클래스는 "Filter" 인터페이스를 상속받아야 하고, 구현해야 하는 세 개의 메소드 중 실제 필터의 기능을 담당하는 doFilter 메소드를 구현할 때 FilterChain 객체를 넘겨주게 됩니다.

 

UsernamePasswordAuthenticationRequest 코드

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class UsernamePasswordAuthenticationRequest {
    private String username;
    private String password;
}

 

 

 

PrincipalDetails 코드

import com.example.bbakmemo.vo.UserVo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;


@Builder
@AllArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final String accountId;
    private final String password;
    private final boolean enabled;
    private final List<? extends GrantedAuthority> getAuthorities;
    private final UserVo userVo;

    public UserVo getUserVo() {
        return userVo;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return getAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return accountId;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

UserDetails란?

  • 인증에 성공하여 생성된 UserDetails 객체는 UsernamePasswordAuthenticationToken을 생성하기 위해 사용됩니다.
  • UserDetails 인터페이스의 경우 직접 개발한 User 엔티티나 UserDto에 UserDetails를 구현하여 처리할 수 있습니다.

 

 

PrincipalDetailsService 코드

import com.example.bbakmemo.mapper.UserMapper;
import com.example.bbakmemo.vo.UserVo;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserMapper userRepository;

    @Override
    public UserDetails loadUserByUsername(String accountId) throws UsernameNotFoundException {

        UserVo userVo =  userRepository.findUserByAccountId(accountId)
                .orElseThrow(
                        () -> new UsernameNotFoundException("유효하지 않은 로그인 정보입니다.")
                );

        return PrincipalDetails.builder()
                .accountId(userVo.getAccountId())
                .password(userVo.getPassword())
                .enabled(userVo.isEnabled())
                .build();
    }
}

UserDetailsService란?

  • DB에서 유저 정보를 불러오는 중요한 메소드가 있는 인터페이스이다.
    => loadUserByUsername() 메소드
  • DB에서 유저의 정보를 가져와서 리턴해준다.

 

 

WebSecurityConfig 코드

import com.example.bbakmemo.config.security.jwt.JwtTokenVerifier;
import com.example.bbakmemo.config.security.jwt.JwtUserNamePasswordAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


/* WebSecurityConfigurerAdapter란?
    스프링 시큐리티의 웹 보안 기능 초기화 및 설정
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // BCryptPasswordEncoder는 Spring Security에서 제공하는 비밀번호 암호화 객체
    // 비밀번호를 BCryptPasswordEncoder로 암호화 안하면 Spring Security 자체에서 경고를 하게 된다.
    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }


    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        // h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
        web.ignoring()
                .antMatchers("/h2-console/**")
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .headers().frameOptions().disable().and()
                .csrf().disable()
                .formLogin().disable()

                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .addFilter(new JwtUserNamePasswordAuthenticationFilter(authenticationManager()))
                .addFilterAfter(new JwtTokenVerifier(), JwtUserNamePasswordAuthenticationFilter.class)
                .authorizeRequests()

                // 회원 관리 처리 API 전부를 login 없이 허용
                .antMatchers("/user/signup").permitAll()
                .anyRequest()
                .authenticated();
    }
}

 

 

 

❓ 세션기반 인증과 토큰기반 인증의 차이

  • 세션의 경우 Cookie 헤더에 세션 ID만 실어 보내면 되므로 트래픽을 적게 사용한다.
  • JWT는 사용자 인증 정보와 토큰의 발급시각, 만료시각, 토큰의 ID 등 담겨있는 정보가 세션 ID에 비해 비대하므로
    세션 방식보다 훨씬 더 많은 네트워크 트래픽을 사용한다.
  • 세션의 경우 모든 인증 정보를 서버에서 관리하기 때문에 보안 측면에서 조금 더 유리하다.
    => 설령 세션 ID가 해커에게 탈취된다고 하더라도, 서버측에서 해당 세션을 무효 처리하면 된다.
    => 토큰의 경우 서버가 트래킹하지 않고, 클라이언트가 모든 인증정보를 가지고 있다.
    따라서 토큰이 한번 해커에게 탈취되면 해당 토큰이 만료되지 전까지는 속수무책으로 피해를 입을 수 밖에 없다.
    * JWT 토큰의 보안 문제 때문에 Access Token과 Refresh Token을 사용해서 보안 대책을 세우는 방법도 있다.
    (리프레쉬 토큰 관련 참고링크로 이동)

 

 

JWT가 여러 문제가 있음에도 사용하는 이유

  • 확장성이 좋다.
    일반적으로 웹 애플리케이션의 서버 확장 방식은 수평 확장을 사용한다. 즉, 한대가 아닌 여러대의 서버가 요청을 처리하게 된다. 이때 별도의 작업을 해주지 않는다면, 세션 기반 인증 방식은 세션 불일치 문제를 겪게 된다.
    이를 해결하기 위해서 Sticky Session, Session Clustering, 세션 스토리지 외부 분리 등의 작업을 해주어야 한다.

    하지만, 토큰 기반 인증 방식의 경우 서버가 직접 인증 방식을 저장하지 않고, 클라이언트가 저장하는 방식을 취하기 때문에 이런 세션 불일치 문제로부터 자유롭다. 이런 특징으로 토큰 기반 인증 방식은 HTTP의 비상태성(Stateless)를 그대로 활용 할 수 있고, 따라서 높은 확장성을 가질 수 있다.

  • 서버의 부담이 줄어든다.
    세션 기반 인증은 서비스가 세션 데이터를 직접 저장하고 관리하지만 토큰 인증 방식은 클라이언트가 인증 데이터를 직접 가지고 있다. 따라서 유저의 수가 얼마나 되던 서버의 부담이 증가하지 않는다.

 

 

❓ Authorization header란

Authorization 헤더는 인증 토큰(JWT든, Bearer 토큰이든)을 서버로 보낼 때 사용하는 헤더입니다.

API 요청같은 것을 할 때 토큰이 없으면 거절당하기 때문에 이 때 Authorization을 사용하면 됩니다.

 

 

 

 

❓ 헤더에 Bearer을 적는 이유

JWT 혹은 OAuth에 대한 토큰을 인증 타입을 명시하는 것이다.(RFC 6750)

결국 인증 시스템 구축을 위한 약속된 틀입니다.

다른 키워드를 넣어서 개발 할 수는 있지만 약속된 틀을 깨는 것이기 때문에 차 후에 문제가 발생할 수 있습니다.

반응형

 

Entity에 게시글 수정 로직을  작성

@Getter
@NoArgsConstructor
@Entity
public class Board extends Timestamped {

   ...

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }

   ...
   
}
  • 비즈니스 로직을 엔티티에 구현하는 것은 도메인 주도 개발 방식이다.
  • 위와 같은 방식은 서비스 부분을 좀 더 가볍게 만들 수 있습니다.
    반대로 엔티티에 비즈니스 로직을 구현하지 않으면 서비스 부분이 코드가 많이 길어집니다.
  • 엔티티에 비즈니스 로직을 구현하는 것은 엔티티를 객체로 사용
    엔티티를 단순히 데이터 전달 역할을 하게 만드는건 엔티티를 자료구조로 사용
  • 클린코드 '6. 객체와 자료구조' 에 둘의 차이가 자세히 설명 되어있음

 

 

서비스단에 게시글 수정 로직 작성(JPQL 사용)

   public void updateBoard(Long id, BoardRequestDto requestDto, UserDetailsImpl userDetails) {

        loginCheck(userDetails);

        Optional<Board> findBoard = boardRepository.findById(id);

        Board board = boardValidCheck(findBoard);

        Long boardId = board.getPostId();
        String boardTitle = board.getTitle();
        String boardContent = board.getContent();

        boardRepository.updateBoard(boardId, boardTitle, boardContent);
    };
  • 아직은 간단한 프로젝트라서 서비스 로직이 단순하고 짧습니다.
    그런데 확실히 로직이 복잡해지고 기능이 많이 추가되면 서비스 부분이 많이 무거워질 것 같습니다.
  • 서비스 부분에 비즈니스 로직을 모아서 작성하면 역할을 확실하게 분담 할 수 있습니다.
  • 경험상으로 비즈니스 로직만 구현되어 있기 때문에 정리가 훨씬 편했습니다.

 

 

 

느낀점

  • 리팩토링을 하기 전에는 엔티티에 비즈니스 로직 코드가 있으면 절대 안되는 걸로만 알고 있었습니다.
    하지만 각각 장단점이 있다는 것을 자료를 찾아보면서 알게 되었습니다.
  • 각각 장단점을 알기 위해서는 우선 공부와 경험이 필요하다고 뼈저리게 느꼈습니다.
  • JPA 관련 공부는 DB 공부를 하면서 같이 진행
    • 우선 JPA를 배우는데 욕심 부리지 않고 DB 관련 공부에 더 집중
  • 엔티티에 @Setter 어노테이션은 달면 안된다고 생각했는데 그게 꼭 맞지는 않는다는 걸 알게되었습니다.

 


참고자료

  • 깃허브 블로그 [joont92] JPQL => 링크
  • 인프런 비즈니스 로직구현 Entity VS Service => 링크

 

 

 

반응형

 

Jasypt란?

개발자가 암호화 작동 방식에 대한 깊은 지식 없이도 최소한의 노력으로 자신의 프로젝트에 기본 암호화 기능을

추가할 수 있도록 하는 Java 라이브러리입니다.

 

자바 외에도 Java + Spring, Java + Hibernate 등 여러 프레임 워크에서 암복호화를 지원해주는 오픈소스입니다.

 

저는 이 Jasypt를 활용해서 yaml 파일에 있는 민감한 정보(DB 계정 비밀번호 등)을 암호화 하는데 사용했습니다.

 

 

 

1. build.gradle에 dependency 추가

    // Jasypt
    implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'

 

 

 

 

2.  config 폴더에 JasyptConfig 파일 생성 및 (아래의)코드 추가

@Configuration
public class JasyptConfig {

    @Value("${jasypt.encryptor.password}")
    private String encryptKey;

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor(){
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(encryptKey);
        config.setPoolSize("1");
        config.setAlgorithm("PBEWithMD5AndDES");
        config.setStringOutputType("base64");
        config.setKeyObtentionIterations("1000");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        encryptor.setConfig(config);
        return encryptor;
    }
}

 

 

 

3.  테스트 코드로 Jasypt를 사용해 암호화

public class JasyptTest extends JasyptConfig {

    @Test
    public void jasypt_encrypt_decrypt_test() {
        String plainText = "plainText";

        StandardPBEStringEncryptor jasypt = new StandardPBEStringEncryptor();
        jasypt.setPassword("password");

        String encryptedText = jasypt.encrypt(plainText);
        String decryptedText = jasypt.decrypt(encryptedText);

        System.out.println(encryptedText);

        assertThat(plainText).isEqualTo(decryptedText);
    }
}
  • Jasypt를 사용해 각 값들을 암호화 합니다.
  • 테스트를 실행해서 나온 암호화된 데이터들을 yml에 입력합니다.

* 테스트 코드 없이 아래의 사이트에서도 암호화 및 복호화가 가능합니다.

https://www.devglan.com/online-tools/jasypt-online-encryption-decryption

 

Programming Blog Article Feeds as per your Interest | DevGlan

Best programming article feeds as per your Interest on different technologies. Subscribe to any technology and explore the best articles from around the web.

www.devglan.com

 

 

 

4. 암호화된 값 yml에 입력

  • 암호화된 값을 입력할 때는 ENC(암호화된 값) 형식으로 입력합니다.
  • jasypt에 대한 yml 값도 위 사진처럼 입력합니다.

 

5. jasypt 키값 VM option에 추가

키값을 yml에 그냥 추가하는 방법도 있지만 그러면 암호화한 값을 복호화 하는 키가 노출 되는 것이기 때문에 VM option을 통해
환경변수로 키 값을 입력하는 방식을 사용했습니다.

 

인텔리제이 상단 메뉴에서

Run -> Edit Configurations

 

 

위에 해당하는 부분에 

-Djasypt.encryptor.password=1234

의 형식으로 비밀번호(키 값)을 입력해줍니다.

 

 

그리고 build.gradle의 test 부분에 아래의 문구를 추가해줍니다.

systemProperty "jasypt.encryptor.password", System.getProperties().get("jasypt.encryptor.password")

 

이렇게 하면 모든 세팅이 완료되었습니다.

고생하셨습니다.

 


궁금한 점

  • Jasypt를 활용하는 다른 방법은 없을지?
    암호화를 하는 라이브러리이기 때문에 다른 곳에도 사용할 수 있지만
    스프링에서는 스프링 시큐리티의 PasswordEncoder가 있어서 굳이 사용할 이유는 없는 것 같습니다.

  • PBEWithMD5AndDES 알고리즘 이란?
    -> 문자 그대로 풀어 보면 "MD5와 DES를 이용한 패스워드 기반 암호화" 정도로 해석될 수 있습니다.
    일반적으로 취약하다고 알려져 있는 MD5와 DES 알고리즘을 사용한다
    PBE(Password Based Encryption, 암호 기반 암호화) 알고리즘은 암호를 기반으로 하는 암호화 알고리즘으로
    암호는 사용자가 스스로 파악하고 무작위 수를 섞어 다중 암호화하는 방법으로 데이터의 안정성을 확보하는 것이 특징이다.

 

 

참고한 자료들

  • (벨로그 sixhustle) Jasypt => 링크
  • (벨로그 haeny-dev) [Jasypt] yaml 파일의 암호화 => 링크

 

 

 

 

 

반응형

JPA

 

 

 

간단한 게시판 프로젝트에서 게시글 수정을 하는 메서드를 호출 하는데 위와 같은 에러가 발생했습니다.

* JPQL을 사용해서 update 쿼리문을 작성했습니다.

 

 

 

구글링을 한 결과 해당 메서드에 @Transactional 어노테이션을 달거나,

Repository에 작성한 JPQL문 위에 @Transactional 어노테이션을 다는 방식으로 해결이 가능했습니다.

 

해당 메서드 위 혹은 JPQL문 위에 트랜잭셔널 어노테이션 기입

 

여기서 왜 @Transactional 어노테이션 의문이 생겼습니다.

구글링을 했지만 해당 내용에대해 명쾌하게 정리되어 있는 글이 없었습니다.

 

그래서 update 쿼리문에서 Transaction 이 필요한 이유를 검색 했습니다.

그 결과 update 문은 해당 테이블의 컬럼의 모든 데이터를 변하게 할 수 있는 강력한 쿼리문이기 때문에,

실수로 작성을 하게되면 엄청난 문제를 초래한다는 내용을 찾게 되었습니다.

 

https://woogie-db.tistory.com/13

 

[MSSQL] 트랜잭션 설명 및 사용법

오늘은 MS-SQL의 UPDATE문을 전체적으로 설명드리겠습니다. UPDATE문 역시 주의해야하는 명령어이므로 꼭 트랜잭션 안에서 사용해야 합니다...... (SELECT문을 제외하고는 대부분 주의가 필요합니다....

woogie-db.tistory.com

 

위 글을 보면 update 발생하는 문제를 방지하기 위해 select문을 먼저 실행 후에 update 쿼리문을 사용하는 방식을 권합니다.

여기서 select 문과 update문을 트랜잭션으로 묶어서 해당 쿼리문을 실행하다 문제가 생기면 롤백하는 것입니다.

 

JPA에서도 이런 문제를 방지하기 위해 update문을 실행하기 전에 select문이 먼저 실행되는 것을 볼 수 있었습니다.

결국 JPA가 우리가 주의해야 할 점들을 알아서 처리해준다는 것을 알게 되었습니다.

 

위 에러를 겪고 고민하고 해결하면서 코딩을 하면서 내가 개선하고 해야할 것들에 대해 정리해봤습니다.

개선할점

  • 결국 JPA를 사용하기 전에 SQL에 대한 공부가 선행 되어야 한다는 것을 느꼈습니다.
  • JPA 사용 시 show-sql 설정을 활용해서 호출하는 쿼리문을 로그에서 확인
    • 호출되는 쿼리문을 보고 하이버네이트의 내부동작에 대해서 공부 필요

 

 

반응형

 

참고자료

  • 우아한 테크코스 블로그 [Tecoble] 3기 케빈님의 글 => 링크
  • 작성자의 게시판 프로젝트 커밋 메시지 => 링크 

QueryDSL을 교육기관을 들으면서 사용했던 적이 있습니다.

그때는 너무 바쁘게 적용하고 QueryDSL을 사용하는 이점과 기존 쿼리문을 작성하는 것과의 차이를 정확하게 알지 못했습니다.

그런데 이번에 혼자 게시판 프로젝트를 리팩토링하면서 이유를 알게 되었습니다.

 

 

 

😱 문제가 발생한 부분(위: 수정전, 아래: 수정 후)

 

정말 단순한 쿼리문이지만 잘못 작성해서 문제가 발생했습니다.

 

여기서 큰 문제는 직접 해당 프로그램을 돌려보는 상황에서 문제를 발견 했다는 것입니다.

우선적으로 프로그램을 직접 돌려보면서 버그를 찾고 해결하는것은 상당히 비효율적입니다.

그리고 만약에 로컬에서 돌려보지 않고 그냥 git push를 한다면 해당 버그를 팀원에게 공유 하는 문제를 초래할 수도 있다고 생각했습니다.

 

 

여기서 QueryDSL을 사용하면 자바 코드로 쿼리문을 작성하기 때문에 IDE가 버그를 잡아낼 수 있게 됩니다.

쿼리문을 직접 작성하면 숙련된 프로그래머도 오타가 발생하면 버그가 생기게 된다고 생각합니다.

그런 문제를 거의 완벽하게 잡아 줄 수 있는 이점이 있다고 생각합니다.

 

지금 제가 작성한 쿼리문은 아주 적지만 더 많이 작성한다고 하면 버그가 한개만 생긴다는 보장은 없을 것 같습니다.

 


😀 결론

  • 쿼리DSL을 통해 반복적인 쿼리문에서 나오는 실수를 IDE가 잡아내도록 하자
  • 숙련된 프로그래머도 오타로 인한 버그가 발생하는 경우가 있다고 한다. 이런 문제를 IDE가 잡아내도록 해보자
  • 쿼리DSL을 사용할 수 없는 상황에서 쿼리문에서 버그를 잡아내는 방법을 고안해보자

 

반응형

📚 참고자료

  • [10분 테코톡] 샐리의 트랜잭션 => 링크

0️⃣ 트랜잭션 개념 공부를 하게 된 계기

스프링을 사용하면서 @Transactional 이라는 어노테이션을 적용해본 적이 있습니다.

메서드 위에 해당 어노테이션을 적용하면 메서드 안에 있는 쿼리들이 하나의 단위로 묶인다는 대략적인 용도만 고 있습니다. 그래서 @Transactional 어노테이션의 정확한 개념을 모르고 사용했기 때문에 의문점이 많이 들었습니다.

  • 트랜잭션이란?
  • 트랜잭션을 사용해야 하는 상황은?
  • 스프링에서는 트랜잭션을 어떻게 지원하는지?

 

 

 

 

1️⃣ 트랜잭션이란?

  • 더이상 나눌 수 없는 가장 작은 하나의 단위를 의미
  • 데이터베이스에서는 트랜잭션을 조작함으로써 사용자가 데이터베이스에 대한 완전성을 신뢰할 수 있도록 함
  • 모든 데이터베이스는 자체적으로 트랜잭션을 지원
  • 하나의 명령을 실행했을 때 데이터베이스가 온전히 그 명령을 실행해주는 것
    => 성공하면 커밋(Commit)
    => 실패하면 롤백(Rollback)
  • 데이터베이스는 기본적으로 트랜잭션을 관리하기 위한 설정을 갖고 있습니다.
    • 명령을 끝마칠 때까지 수행 내역을 로그에 저장해둡니다.
      - 데이터베이스에 반영된 내용을 재반영하기 위한 redo log
      - 수행을 실패해 이전의 상태로 되돌리는 undo log
      * 두 개의 log를 이용해 트랜잭션을 지원합니다.

 

 

 

2️⃣ 트랜잭션을 사용하는 상황(예시)과 성질

트랜잭션에서 가장 흔히 볼 수 있는 입출금을 예시로 들어보겠습니다.

 

  1. A가 B에게 만원을 송금하려고 합니다.
  2. A의 계좌에 만원보다 많은 금액이 있는지 확인합니다.
  3. A의 계좌에 만원보다 많은 금액이 있다면 만원을 차감합니다.
  4. 차감한 만원의 금액을 B의 계좌에 더해줍니다.

 

위 업무는 순서를 나눠놨지만 절대로 분리되거나 일부만 실행되면 안 되는 하나의 작업입니다.

이렇게 절대로 깨져서는 안되는 하나의 작업을 트랜잭션이라고 합니다.

 

 

트랜잭션의 네가지 성질

  • 원자성(Atomicity)
    - A가 B에게 송금을 하는데 A의 계좌에서 만원이 차감만 되고 작업이 종료되면 문제가 발생합니다.
    - 이렇듯 일부만 실행되는 경우는 없다는 원자성을 지닙니다.
    - [롤백]1번부터 4번까지 연결된 하나의 작업에서 어느 하나의 명령이라도 실패하면 롤백을 하게 됩니다.
    - [트랜잭션 커밋]반대로 1번부터 4번까지 작업이 성공적으로 수행하면 수정된 내용 데이터베이스에 반영
    - 롤백이나 커밋이 실행되어야 트랜잭션이 종료됩니다.

  • 일관성(Consistency)
    - 데이터베이스 내의 상태 혹은 계층 관계
    - 컬럼의 속성이 항상 일관되게 유지되어야 함
    - 컬럼의 속성이 수정되었다면 trigger를 통해 일괄적으로 모든 데이터베이스에 적용해야 합니다.
  • 지속성(Durability)
    - 트랜잭션이 성공적으로 수행되어 커밋되었다면 어떠한 문제가 발생하더라도 데이터베이스에 그 내용이
    영원히 지속되어야한다는 지속성이 있습니다.
    - 이를 위해 모든 트랜잭션은 로그로 남겨져 어떠한 장애에도 대비할 수 있도록 합니다.

  • 독립성(Isolation)
    - 트랜잭션 수행 시 다른 트랜잭션이 끼어들 수 없고 각각 독립적으로 실행된다는 성질
    DB 독립성의 문제점(더보기 클릭)
    더보기
    하지만 데이터베이스에 작업이 들어왔을 때 모든 작업의 독립성을 보장해 하나씩 순차적으로 진행하게 된다면, CPU는 DBMS보다 인풋, 아웃풋이 빈번히 수행되기 때문에 트랜잭션을 순차적으로 시행하면 CPU는 점점
    응답을 기다리는 시간이 길어지게 됩니다. 그 결과 프로그램이 비효율적으로 동작하는 문제가 발생합니다.

 

이처럼 데이터베이스에 저장된 데이터의 무결성과 동시성의 성능을 지키기 위해 트랜잭션의 설정이 중요한 것입니다.

여러 명령을 하나의 트랜잭션으로 묶고 싶은 경우 개발자가 직접 트랜잭션의 경계설정을 통해 트랜잭션 명시

 

 

 

 

3️⃣ 스프링에서 지원하는 트랜잭션

 

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
    void commit(TransactionStatus status);
    void rollback(TransactionStatus status);
}

 

  • 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 제공
    • getTransaction 메서드
      - 파라미터로 전달되는 TransactionDefinition에 따라 트랜잭션을 시작
      - 트랜잭션을 문제없이 마치면 commit을, 문제가 발생하면 Rollback을 호출
    • getTransaction부터 commit이나 rollback을 하는 부분까지가 트랜잭션의 경계설정입니다.
  • 다양한 DataSource에 맞게 트랜잭션을 관리

 

 

스프링이 제공하는 다양한 트랜잭션 매니저 구현체

 

  1. DataSourceTransactionManager
    - JDBC에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  2. JpaTransactionManager
    - JPA에 사용되는 매니저
    - 하나의 데이터베이스를 사용하거나 각각의 데이터를 독립적으로 사용하는 로컬 트랜잭션에 사용

  3. JtaTransactionManager
    - 하나 이상의 데이터베이스가 참여하는 경우 글로벌 트랜잭션에 사용되는 JtaTransactionManager 사용
    - 여러 개의 데이터베이스에 대한 작업을 하나의 트랜잭션에 묶을 수 있고, 다른 서버에 분산된 것도 묶을 수 있음

 

 

🍃 어노테이션을 기반으로 트랜잭션을 설정하는 방법

 

  @Transactional
    public void addRestaurantFoods(
            Long restaurantId,
            List<FoodRequestDto> requestDtoList
    ) {
        Optional<Restaurant> foundRestaurant = restaurantRepository.findById(restaurantId);

        checkRestaurant(foundRestaurant);
        Restaurant restaurant = foundRestaurant.get();

        for (FoodRequestDto requestDto : requestDtoList) {
            String foodName = requestDto.getName();
            int foodPrice = requestDto.getPrice();

            checkDuplicateRestaurantFood(restaurant, foodName);

            checkFoodPrice(foodPrice);

            Food food = Food.builder()
                    .name(foodName)
                    .price(foodPrice)
                    .restaurant(restaurant)
                    .build();

            foodRepository.save(food);
        }
    }

 

  • 어노테이션이 적용된 메서드는 메서드 시작부터 트랜잭션이 시작
    => 메서드를 성공적으로 끝마치면 트랜잭션 커밋
    => 도중에 문제가 발생하면 롤백
  • 어노테이션은 데이터베이스에 여러번 접근하면서 하나의 작업을 수행하는 서비스 계층 메서드에 붙인다.

 

 

 

반응형

 

 

📚 참고자료

  • 유튜브 채널 [우아한 Tect] - 10분 테코톡 아마찌의 ORM vs SQL Mapper vs JDBC

https://www.youtube.com/watch?v=VTqqZSuSdOk&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=66 

 

 


🤔 영속성(Persistence)이란?

  • 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터의 특성
  • 객체지향 프로그램에서 객체의 상태는 단지 메모리에서만 존재하고 프로그램이 종료되면 휘발하는 형태
    => 영속성을 가지지 않은 상태
    => 객체의 상태를 DB에 저장하면서 객체에게 영속성을 부여함

 

 

  • 위 사진은 Mark Richards의 소프트웨어 아키텍처 패턴 => 이미지링크
  • Persistence 계층에서 Domain Model 계층에 영속성을 부여하는 역할을 합니다.
  • JDBC, SQL Mapper로 Persistence Layer를 구현할 수 있습니다.

 

 

 

 

 

⏺ JDBC만을 이용하여 Persistence Layer 구현하기

 

 

JDBC란?

  • Java Database Connectivity의 약자
  • 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API
  • 자바 애플리케이션에서 DBMS의 종류에 상관없이, 하나의 JDBC API를 이용해 DB 작업을 처리
  • 각각의 DBMS는 이를 구현한 JDBC 드라이버를 제공한다.

 

 

JDBC의 단점

  • 간단한 SQL을 실행하는 데도 중복된 코드를 반복적으로 사용
  • Connection과 같은 공유 자원을 제대로 릴리즈(반환) 해주지 않으면 시스템의 자원이 바닥나는 버그 발생
  • DB에 따라 일관성 없는 정보를 가진 채로 Cheked Exception (SQLException)처리
    • Cheked Exception이란?
      => Checked Exception은 컴파일 단계에서 확인 가능한 예외이다.
      => 다른 말로는 "Compiletime Exception"이라고 한다.
      => Checked Exception은 try/catch로 감싸거나 throw로 던지는 처리를 반드시 해주어야 한다.
      => 예외처리를 컴파일러가 강제하는 것이다.

 

 

 

 

 

 

 

 

 

⏺ Persistence Framework로 Persistence Layer 구현하기

 

SQL Mapper란?

  • 자바 Persistence Framework 중 하나입니다.
  • SQL을 직접 작성합니다.
  • SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객체화

 

 

SQL Mapper의 장점

  • 쿼리 수행 결과와 객체의 필드를 맵핑 & RowMapper 재활용
  • JdbcTemplate가 JDBC에서 반복적으로 해야 하는 많은 작업들을 대신해줌

 

 

SQL Mapper에 속하는 대표적인 프레임워크로 MyBatis가 있습니다.

 

 

 

 

 MyBatis란?

  • 반복적인 JDBC 프로그래밍을 단순화합니다.
  • SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL을 분리하여 관리합니다.
  • JDBC만 사용하면 결과를 가져와서 객체의 인스턴스에 매핑하기 위한 많은 코드가 필요하겠지만,
    마이바티스는 그 코드들을 작성하지 않아도 되게 해 줍니다.
  • 동적 쿼리를 작성할 수 있습니다.
    • 동적 쿼리란?
      => 

 

 

 

  • 위 이미지는 마이바티스가 데이터를 Access 하는 순서를 나타내는 것입니다.
  • 스프링 부트에서 구현할 때 보라색 부분인 Mapper Interface와 Mapping File을 구현하게 되면
    자연스럽게 객체의 필드와 SQL문이 매핑되게 됩니다.
  • Mapper Interface에 대한 구현체를 우리가 구현하지 않아도 마이바티스에서 자동으로 생성하게 됩니다.

 

 

 

마이바티스의 장점

  • 자동으로 Connection 관리를 해주면서 JDBC를 사용할 때의 중복 작업 대부분을 없애준다.
  • 복잡한 쿼리나 다이나믹하게 변경되는 쿼리 작성이 쉽다.
  • 관심사 분리
    • DAO로부터 SQL문을 분리하여 코드의 간결성 및 유지보수성을 향상할 수 있습니다.

 

 

 

😓 JDBC와 마이바티스의 단점

  • SQL을 직접 다룸으로 문제점이 생기게 된다.
  • 특정 DB에 종속적으로 사용하기 쉽습니다.
  • 테이블 마다 비슷한 CRUD SQL 작성은 DAO 개발이 매우 반복되는 작업입니다.
  • 테이블 필드가 변경될 시 이와 관련된 모든 DAO의 SQL문, 객체의 필드 등을 수정해야 합니다.
  • 코드상으로 SQL과 JDBC API를 분리했다 하더라도 논리적으로 강한 의존성을 가지고 있습니다.
  • SQL문을 직접 작성하면 SQL에 의존적인 개발을 하게 됩니다.
    • 데이터 베이스에 항상 의존 관계를 가지게 됩니다.

 

 

 

 

 

ORM이란?

  • 객체와 관계형 데이터베이스를 매핑하는 것
  • 객체간의 관계를 바탕으로 SQL문을 자동으로 생성하고 직관적인 코드(메서드)로 데이터를 조작 할 수 있습니다.'
    • SELECT * FROM user; -> user.findAll();
  • ORM의 대표적인 예로 JPA가 있습니다.

 

 

ORM의 장점

  • 패러다임 불일치 문제 해결
    => 객체지향 언어가 가진 장점을 활용할 수 있습니다.

  • 생산성
    => 지루하고 반복적인 CRUD 용 SQL을 개발자가 작성하지 않아도 됩니다.

  • 데이터 접근 추상화, 벤더 독립성
    => 데이터베이스 벤더마다 미묘하게 다른 데이터 타입, SQL을 손쉽게 해결
  • 유지보수
    => 필드 추가, 삭제 시 관련된 CRUD 쿼리를 직접 수정하지 않고, 엔티티를 수정하면 됩니다.

 

 

ORM의 단점

  • 복잡한 쿼리 사용이 어렵습니다.
    • JPA에서는 SQL과 유사한 기술인 JPQL을 지원합니다.
    • SQL 자체 쿼리를 작성할 수 있도록 지원합니다.
    • SQL Mapper와 혼용해서 사용도 가능합니다.

 

 


 

😃 느낀점

  • 현재(작성일) 사이드 프로젝트에서 SQL Mapper인 마이바티스를 사용하고 있습니다.
    • 직접 사용해보면서 장점과 단점을 생각해볼 예정입니다.
  • SQL Mapper를 사용하는 프로젝트를 끝낸 후에 JPA를 사용하는 프로젝트 진행
    • 부트캠프에서 Spring Data JPA를 사용했지만 너무 얕게 공부했다는걸 영상을 보고 알게되었습니다.
  • JPA를 김영한님 책과 인프런 강의를 보면서 공부할 예정입니다.

 

 

 

 

 

📝 해야할 것!

  • 사이드 프로젝트를 통해 SQL Mapper(마이바티스)의 장점과 단점을 사용하면서 알아가기
  • 사이드 프로젝트가 끝난 후에 JPA에 대한 공부를 진행
    • JPA 공부를 통해 ORM에 대한 개념을 이해하고 사용하는 이유와 장단점을 알아가기
    • JPA에서 SQL을 처리하는 구조를 공부하면서 사용해보기

 

 

 

 

반응형

+ Recent posts